API overview
The token API's address, API tokens, answer format, status codes, errors and the sign-in lock-out.
The token API lets a program manage HF Panel without a browser: hosting accounts, packages and resellers, domains, DNS, email, databases, SSL, backups, cron jobs, FTP accounts and the server itself. Billing systems, your own scripts and the HF Server desktop app use it. This page covers the rules shared by every endpoint. The endpoints themselves are in the API reference.
The address
| Address | When to use it |
|---|---|
https://api.yourdomain.com:2087/v1 |
the API's own address; use it for new code |
https://api.yourdomain.com/v1 and https://api.yourdomain.com:8443/v1 |
the same API on ports 443 and 8443, for clients that cannot use port 2087 |
https://panel.yourdomain.com:8443/api/v1 |
the old address; it keeps working and gives the same answers |
The paths after the base are the same everywhere: GET https://api.yourdomain.com:2087/v1/accounts is the same call
as GET https://panel.yourdomain.com:8443/api/v1/accounts. HF Panel never redirects from the old address to the new
one, because a redirect would drop the body of a POST and the Authorization header.
Note: a server has
api.yourdomain.comonly when the separate panel addresses are switched on. Without it, use the old address.GET /api/v1on the old address tells you the new address inbasewhen there is one.
Programs only
api.yourdomain.com answers only programs. A request that looks like a person's browser opening a page (it asks for
HTML, or sends Sec-Fetch-Mode: navigate) and has no Authorization header gets 403 Forbidden, on every port.
There are no panel pages, no session cookies and no CORS headers on this address.
GET https://api.yourdomain.com/answers{"name": "HF Panel API", "version": "v1", "docs": "https://api.yourdomain.com:2087/v1"}. The name follows your brand (white label).GET /v1lists the endpoints and needs no token.GET /v1/openapi.jsonis the OpenAPI 3.1 description and needs no token.- Any other address answers
404with{"ok": false, "error": "Not found."}.
curl -s https://api.yourdomain.com:2087/v1
{
"ok": true,
"data": {
"name": "HF Panel API",
"version": "v1",
"auth": "Authorization: Bearer <token>",
"base": "https://api.yourdomain.com:2087/v1",
"old_base": "https://panel.yourdomain.com:8443/api/v1",
"openapi": "https://api.yourdomain.com:2087/v1/openapi.json",
"endpoints": [
"GET /v1/me",
"GET /v1/packages",
"GET /v1/accounts",
"…"
]
}
}
Authentication: API tokens
Every request, except GET /v1 itself, sends an API token in the Authorization header:
Authorization: Bearer hf_xxx
A token is hf_ followed by 48 lower-case hexadecimal characters. No cookie and no CSRF token are needed: the token is
the proof. HF Panel keeps only a SHA-256 hash of each token, so it cannot show a token again after it is made.
Make a token
Where: your name at the top right › Password & security › API tokens (in Server Admin, Reseller Panel or Control Panel)
- In Name, type what the token is for, for example
Billing system. - Choose the Access:
- Read only: the token can only read.
- Read & change: the token can also create, change and delete.
- Optionally, fill in Allowed IPs: IP addresses or ranges, separated by spaces or commas, for example
203.0.113.5 198.51.100.0/24. Leave it empty to allow any address. - Type your password (or a code from your authenticator app when two-factor login is on), then click Create token.
- Copy the token straight away with Copy token. It is not shown again.
The API tokens table shows each token's name, its first characters, its access, its allowed IPs and when and from where it was last used. Click Delete to stop a token at once. Anything that uses it stops working.
| Rule | What it means |
|---|---|
| Up to 20 tokens per login | delete one before you make another |
| A token acts as the person who made it | an administrator's token sees every hosting account, a reseller's token only their customers, a customer's token only their own account |
| Read-only staff logins | can make only Read only tokens; their tokens never change anything |
| Suspended logins | their tokens stop working (403 "This login is suspended.") |
| Reseller privileges | a reseller's tokens can only do what the administrator allows that reseller under Reseller privileges |
| Not through "Log in as" | tokens are made and deleted only by the person themselves |
Important: treat a Read & change token like a password. Limit it to your billing server's IP address whenever you can.
Tokens for a team role
A customer can also make tokens limited to one team role.
Where: Control Panel › Account › Team › API tokens for a role
Only the account owner can make these, from their own login and with their password. Pick a Role (Full access, Websites & files, Email only, Databases only, Billing only or Read-only) and an Access, then click Make token. These tokens count towards the same limit of 20.
| Endpoint | Allowed for |
|---|---|
/v1/me, /v1/status |
every role |
/v1/domains, /v1/sync/sites, /v1/tunnels |
roles with websites: Full access, Websites & files, Read-only |
/v1/mailboxes |
roles with email: Full access, Email only, Read-only |
/v1/databases, /v1/mongo |
roles with databases: Full access, Databases only, Read-only |
/v1/accounts/{username}/domains, …/dns, …/ssl, …/backups, …/cron, …/ftp |
roles with websites |
/v1/accounts/{username}/email |
roles with email |
/v1/accounts/{username}/mysql, …/postgres |
roles with databases |
A Read-only role token can only use GET. Anything else answers 403 "This token’s team role doesn’t include
this."
IP allow-list
When a token has Allowed IPs, a request from any other address answers 403 "This token cannot be used from
." Each entry is a single address (IPv4 or IPv6) or a range in CIDR form (`198.51.100.0/24`, `2001:db8::/32`).Requests
- Send bodies of
POST,PUTandPATCHrequests as JSON withContent-Type: application/json. A normal form post (application/x-www-form-urlencoded) works too. - In JSON bodies,
truecounts as"1", and numbers may be sent as numbers or text. - The older per-account endpoints (
/v1/domains,/v1/mailboxes,/v1/databases,/v1/mongo,/v1/tunnels) take?account=<username>in the query when you use an administrator's or reseller's token. With a customer's token they always use the token's own account. - The newer ones name the account in the path:
/v1/accounts/{username}/dns/…. A customer's token may only name its own account. Staff work in the account as Log in as does (see per-account endpoints). - A body that is neither JSON nor a form answers 400
Send the body as JSON, e.g. {"username":"param"}.
Answers
Every answer is pretty-printed JSON with Content-Type: application/json; charset=utf-8, and is never cached
(Cache-Control: no-store).
{
"ok": true,
"data": {}
}
{
"ok": false,
"error": "No such account."
}
error is a plain sentence you can show to a person. When a change was made but something around it was not (for
example "Saved, but Stripe was not updated: …"), the answer is still "ok": true and also has a warnings list. Times such as created_at are UTC, written
YYYY-MM-DD HH:MM:SS. A value that is not known is null.
Status codes
| Status | Means |
|---|---|
| 200 | done |
| 201 | created: the POST endpoints that make something (an account, package, reseller, domain, record, mailbox, database, backup, cron job …) |
| 400 | the body is not JSON or a form, or ?account= is missing |
| 401 | no token, a malformed token, or an unknown or deleted token |
| 402 | the paid edition's licence needs attention; the body is {"error": "The control panel licence needs attention: …"} |
| 403 | a Read only token or read-only staff login tried to change something; the endpoint is for administrator and reseller tokens only; the IP address is not allowed; the login is suspended; the feature is not part of the account's plan; the reseller lacks the privilege; the action needs another administrator's approval |
| 404 | no such endpoint, account, domain, record and so on. An account your token may not manage is also "No such account." |
| 409 | a request with the same Idempotency-Key is still running |
| 422 | refused with a message: a bad field, an unknown package, the account limit, an Idempotency-Key already used for another request and so on |
| 429 | the token's request limit is used up (see Retry-After), or too many failed attempts from your IP address (15 minutes) |
| 500 | the server refused the change; the message starts with Server: |
Common error messages
| Status | error |
|---|---|
| 401 | Send an API token: Authorization: Bearer hf_… |
| 401 | Unknown or deleted token. |
| 403 | This token is read-only. Make a "Read & change" token to do this. |
| 403 | This staff login is read-only, so its tokens can only read. |
| 403 | Only administrator and reseller tokens can do this. |
| 403 | This feature is not part of your hosting plan. |
| 403 | Your hosting provider has not given your reseller account this permission. |
| 403 | This needs another administrator's approval first. Do it in the control panel, where it is sent for approval: Delete a hosting account. |
| 400 | Add ?account=<username> to say which account. |
| 404 | No such endpoint: GET /v1/… |
| 429 | Too many failed attempts from this IP. Try again in 15 minutes. |
Idempotency-Key: safe retries
A billing system that sends a POST and loses the answer (a time-out, a dropped connection) cannot know whether the
account was made. Send an Idempotency-Key header with every POST that makes something, and send the same
request again with the same key:
curl -s -X POST -H "$H" -H "Idempotency-Key: order-1001" -H "Content-Type: application/json" \
-d '{"username":"sarahbakes","email":"sarah@example.com","package":"Starter"}' $A/accounts
| Rule | What it means |
|---|---|
| The key | 1–255 printable characters without spaces; for example your order or invoice number. A bad key answers 400 |
| Remembered for 24 hours | per token: the same key with the same request (method, path, query and body) gets the first answer again, with the header Idempotent-Replayed: true, and nothing is done twice |
| Another request | the same key with a different request answers 422 "This Idempotency-Key was already used for another request." |
| Still running | while the first request is still being done, the same key answers 409; try again a moment later |
| What is kept | answers below 500 (refusals too), except 429. A 500 or 429 is not kept, so you can retry it with the same key |
The kept answer is encrypted with the server's key, because it may hold a new password. The key works on POST
only; GET, PUT, PATCH and DELETE are safe to repeat anyway.
Rate limits and lock-out
Each token may make 600 requests a minute (the server's administrator can change this with api_rate_limit in
/etc/hostfog/config.json; 0 switches the limit off). Every answer to a token says where it stands:
| Header | What it says |
|---|---|
X-RateLimit-Limit |
requests this token may make a minute |
X-RateLimit-Remaining |
requests left in this minute |
X-RateLimit-Reset |
when the minute ends, as Unix time (seconds) |
One request more answers 429 "Too many requests with this token: 600 a minute. Try again in N seconds." with a
Retry-After header (seconds). Each token has its own minute.
The token API also shares the sign-in page's lock-out:
- A request with no token, a malformed token, or an unknown or deleted token counts as a failed sign-in for your IP address.
- After 8 failed attempts in 15 minutes, every request from that address answers 429 "Too many failed
attempts from this IP. Try again in 15 minutes." until the 15 minutes have passed, even with a good token. This
answer has no
Retry-After. - IPv4 addresses are counted one by one. IPv6 addresses are counted by their
/64network.
Tip: if a script suddenly gets 429 without
Retry-After, check that it sends the full token with the wordBearerand a space in front of it. Fix the script, then wait 15 minutes.
Pagination
The token API does not page its lists. GET /v1/accounts, GET /v1/domains and the other lists return every item
the token may see in one answer.
Approvals
When the administrators have set an action to need a second administrator's approval, the token API cannot wait for
one. DELETE /v1/accounts/{username}, DELETE /v1/resellers/{username} and a whole restore
(POST /v1/accounts/{username}/backups/{backup}/restore) then answer 403 and the action must be done in the
panel, where it is sent for approval. The same goes for a reseller whose deletes and restores wait for an
administrator. When approvals ask only for your password, the token itself is enough.
The interface's own API
The panels, HF Mail, HF Files and HF Studio are browser apps. They call /api/v1/… on their own address
(for example https://panel.yourdomain.com:8443/api/v1/domains) with a session cookie. That interface API is not meant
for your scripts, and it has stricter rules:
- Every call,
GETincluded, must send the session's CSRF token in theX-CSRF-Tokenheader. The token comes fromGET /api/v1/_boot(data.csrf) and in thecsrffield of every answer; always keep the newest. A wrong or missing token answers 403 with a freshcsrf. - Calls from another site are refused (403): the
Originmust be the same host andSec-Fetch-Sitemust besame-originornone.
On the panel's address, a request with Authorization: Bearer hf_… is answered by the token API instead, with no
cookie and no CSRF token. On api.yourdomain.com only the token API answers: a request that brings only a session
cookie gets the token API's 401.
Tip: for scripts, always use an API token. It does not expire with a session and needs no CSRF token.