Billing Integration API ​
This guide is for developers connecting a billing platform to AdminBolt. It maps a billing system's service lifecycle (order, upgrade, suspend, terminate) onto the AdminBolt REST API, states the guarantees each operation gives you, and shows a worked call for each one.
If you only want to connect an existing billing platform, you do not need this page. Use the ready-made module instead:
| Platform | Module |
|---|---|
| WHMCS | AdminBolt for WHMCS |
| Blesta | bolt-blesta |
| HostBill | bolt-hostbill |
| Upmind | bolt-upmind |
This page documents the API those modules are built on, for platforms that have no module yet and for anyone extending one.
Note: This guide covers the lifecycle and the guarantees. It does not repeat the endpoint reference. Every request and response field is listed in the API Reference, and the panel serves the same specification at
/api/documentation.
Note: This guide describes the billing integration surface added to the panel for exactly this purpose. Confirm the panel is new enough before you build against it:
GET /hosting-accounts?username=<username>has to filter rather than return every account. On an older build there are no lookup filters, a plan can only be addressed byhosting_plan_id, and usage values come back as formatted strings rather than numbers.
Base URL and prerequisites ​
The API is served by the panel itself, on the panel port (8443 unless the panel was installed on another port):
https://panel.example.com:8443/apiBefore you write any code you need:
- An API key pair. An administrator issues one in the panel under Security > API Keys (see API Keys). A reseller issues one in the reseller panel under Security > Api Keys (see Reseller API Keys).
- At least one hosting plan, because a billing product maps onto a plan.
- A valid panel license. With an expired license the API keeps serving reads and refuses writes.
Authentication ​
Send the key and the secret as headers on every request. There is no login call and no session:
X-API-Key: <key>
X-API-Secret: <secret>Warning: Send
Accept: application/jsonon every request as well. Without it, a request that fails field validation is answered with a302redirect and an HTML body instead of a422and a JSON message, because the panel falls back to its browser behaviour. A client that does not set the header sees an unexplained redirect wherever it sends a malformed request, and any HTTP library that follows redirects turns that into a misleading success. SetAccept,Content-Type: application/json, and disable redirect following.
Issue a dedicated key pair per billing system, so you can revoke one integration without disturbing the others, and turn on the per-key IP whitelist for the billing server's address. A key pair is shown in full only when it is created.
Authentication failures are reported consistently:
| Status | error | Cause |
|---|---|---|
401 | API key is missing / API secret is missing | A header was not sent. |
401 | Invalid API key or secret | The pair does not match a key. |
401 | API key is not active | The key exists but is disabled. |
403 | API key does not have access to this resource. | An administrator key was used on the reseller API, or the reverse. |
403 | IP address not whitelisted | The calling address is not on the key's whitelist. |
403 | API key does not have access to this endpoint or method | The key is restricted to a set of endpoints that excludes this one. |
Treat all four of these as configuration errors and surface them to the operator. Retrying does not help.
Rate limits ​
Calls are limited to 120 requests per minute per key and calling address, and 300 requests per minute per calling address. The 121st call within the window is the first to be refused. Every response carries the budget, so a module can pace itself instead of waiting to be refused:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1788521634
Retry-After: 36Beyond either limit the API answers 429, and Retry-After gives the seconds to wait.
One lifecycle operation is usually several calls, because a module looks an account up before acting on it, so plan a usage sync that walks every account accordingly. When you receive a 429, wait for Retry-After and retry rather than failing the operation.
Connection test ​
Use this for the module's Test Connection button. It confirms in one call that the panel is reachable, the license is valid, and the key pair works:
curl -s https://panel.example.com:8443/api/health \
-H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
-H "Accept: application/json"{ "status": "ok" }Hosting plans ​
A billing product maps onto an AdminBolt hosting plan. List the plans to populate the product configuration dropdown:
curl -s https://panel.example.com:8443/api/hosting-plans \
-H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
-H "Accept: application/json"[
{ "id": 1, "name": "Starter", "disk_space": 5000, "bandwidth": 50000 },
{ "id": 2, "name": "Unlimited", "disk_space": 0, "bandwidth": 0 }
]The response carries the full plan objects; see Hosting Plans for every field.
A plan can be addressed by numeric id or by exact name. Storing the name in the billing product is usually the better choice, because it survives a panel rebuild, and plan names are unique. Name resolution is scoped to the owner of the account being created or changed: a plan owned by a reseller is reachable only when the call also names that reseller. An unresolvable name returns 422:
{ "message": "Hosting plan not found: Reseller Gold" }An unknown plan id returns 422 with {"message": "Hosting plan not found"} on both the administrator and the reseller API. A plan id of 0 is refused by field validation rather than by the lookup, so it is one of the responses that needs the Accept: application/json header to arrive as a 422 at all.
Account lifecycle ​
Create an account ​
curl -s -X POST https://panel.example.com:8443/api/hosting-accounts \
-H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
-H "Accept: application/json" -H "Content-Type: application/json" \
-d '{ "domain": "example.com", "hosting_plan": "Starter" }'| Field | Required | Notes |
|---|---|---|
domain | Yes | The account's main domain. It is the account's identity and cannot be changed later. |
hosting_plan | One of the two | Exact plan name. |
hosting_plan_id | One of the two | Numeric plan id. |
username | No | Derived from the domain when omitted. |
password | No | Generated when omitted, and returned in the response. |
reseller_id | No | Creates the account under a reseller. It cannot be changed later. |
ssh_access | No | Defaults to false. |
A successful call returns 201:
{
"id": 42,
"username": "example",
"password": "generated-when-none-was-sent",
"hostingAccount": { "id": 42, "domain": "example.com", "username": "example" }
}Store id as the remote reference for the service. It is returned at the top level for exactly that purpose; the nested hostingAccount.id carries the same value and is kept so that older integrations keep working. username is unique too, so an integration that lost its stored id can re-link on it.
The generated password is returned only in this response. Persist it or hand it to the customer immediately.
A create that fails returns 422 with a readable message. If it failed part way through provisioning, AdminBolt rolls the partial account back, so the same request can be sent again once the cause is fixed and it will not collide with a leftover record.
Warning: Anything that logs full HTTP exchanges will capture the key, the secret, and this generated password. Keep debug logging off in production, and treat debug logs from a test run as credentials.
Look an account up ​
curl -s "https://panel.example.com:8443/api/hosting-accounts?username=example" \
-H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
-H "Accept: application/json"username and domain are exact-match filters, and both fields are unique, so the response is an array of zero or one element. Use this to re-link an imported service, or to check the current state before acting on it.
Send the value exactly as it is stored. A trailing space does not match, and a repeated or array-style parameter (?username[]=example) is refused:
{ "message": "username must be a single string value." }Passing no filter lists every account the key can see, which for an administrator key is all of them. Filter, rather than listing and searching client side.
Change the password ​
curl -s -X POST https://panel.example.com:8443/api/hosting-accounts/42/change-password \
-H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
-H "Accept: application/json" -H "Content-Type: application/json" \
-d '{ "password": "the-new-password" }'Change the package ​
An upgrade or downgrade is an update that names the new plan:
curl -s -X PUT https://panel.example.com:8443/api/hosting-accounts/42 \
-H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
-H "Accept: application/json" -H "Content-Type: application/json" \
-d '{ "hosting_plan": "Unlimited" }'The new limits apply immediately, because they are evaluated live against the assigned plan.
Two fields are the account's identity and are refused rather than ignored, so a billing platform that sends its whole service record on every update learns about the mismatch instead of assuming it took effect:
{ "message": "domain cannot be changed via API." }{ "message": "reseller_id cannot be changed via API." }Both return 422. Send the current value, or leave the field out.
Suspend and unsuspend ​
curl -s -X POST https://panel.example.com:8443/api/hosting-accounts/42/suspend \
-H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
-H "Accept: application/json"
curl -s -X POST https://panel.example.com:8443/api/hosting-accounts/42/unsuspend \
-H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
-H "Accept: application/json"Suspending points the account's domains at a suspension page and marks the account suspended. Both calls are idempotent: suspending an already suspended account, or unsuspending an active one, re-applies the state and returns 200. A billing platform can therefore re-drive its own state without tracking whether it already sent the call.
Terminate ​
curl -s -X DELETE https://panel.example.com:8443/api/hosting-accounts/42 \
-H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
-H "Accept: application/json"This removes the account and its server-side resources. It returns 200 on success and 404 when no such account exists. For a billing flow, treat 404 as already terminated and let the cancellation complete; every ready-made AdminBolt module does. A 422 means the deletion itself failed and needs an operator.
Usage synchronisation ​
curl -s https://panel.example.com:8443/api/hosting-accounts/42/usage-details \
-H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
-H "Accept: application/json"{
"usageDetails": {
"diskUsage": 12.34,
"totalDiskUsage": 1000,
"diskUsageWarning": false,
"databaseDiskUsage": 0,
"totalDatabaseDiskUsage": 100,
"bandwidth": 0,
"totalBandwidth": 50000,
"addonDomains": 1,
"totalAddonDomains": 10,
"databases": 2,
"totalDatabases": 10
}
}The format is fixed so that a module can consume it without parsing:
- Sizes are numbers in MB. There is no unit suffix and no thousands separator, so
9999.95is never rendered as"9,999.95 MB". - Counts and limits are integers, for addon domains, subdomains, alias domains, FTP accounts, databases, email accounts, email forwarders, and email autoresponders.
- A limit of
0means unlimited. Map it to whatever your platform uses for no limit, and never to a quota of zero. diskUsageWarningis a boolean. It istrueonly when usage exceeds the plan allocation, and it is nevertrueon an unlimited plan.
Poll this from the billing platform's usage cron. Daily is typical, and it keeps a fleet-wide sync inside the rate limit.
Client single sign-on ​
curl -s -X POST https://panel.example.com:8443/api/hosting-accounts/42/generate-sso-token \
-H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
-H "Accept: application/json"{
"redirect_url": "https://panel.example.com:8443",
"sso_url": "https://panel.example.com:8443/hosting-account/sso-login/<token>",
"token": "<token>"
}Redirect the customer's browser to sso_url, which lands them in the client panel. Use that field as it is returned, and do not rebuild the URL from redirect_url: redirect_url is the panel's own canonical address, which is often a different hostname from the one your request went to. The semantics a client area has to respect:
- The token is valid for 30 minutes and is single-use. It is consumed on first login and cleared on expiry.
- One token is active per account. Generating a new one replaces the previous one, so two client-area windows cannot hold two working links.
- Generate on demand, when the customer clicks the log-in button, never in advance and never at page render.
- If the customer has two-factor authentication enabled on the hosting account, the link takes them to the second-factor challenge rather than straight into the panel. This is expected; do not report it as a failed sign-on.
A token that has expired, has already been used, or does not exist sends the browser to the client login page instead.
Errors and idempotency at a glance ​
| Operation | Repeated call | Account does not exist |
|---|---|---|
| Create | 422, the domain is already taken | Not applicable |
| Look up | Same result | Empty array |
| Change password | Applies again, 200 | 404 |
| Change package | Applies again, 200 | 404 |
| Suspend | State re-applied, 200 | 404 |
| Unsuspend | State re-applied, 200 | 404 |
| Terminate | 404, treat as already terminated | 404 |
Rules for module authors:
2xxis success. Themessagefield is written for a person and is safe to show to the operator.422carries a readablemessagedescribing what was rejected. Surface it in the billing admin interface instead of a generic failure.404on a lifecycle operation means the account is gone from the panel. Treat it as success for a termination, and flag it for manual review for anything else.401and403are credential or whitelist problems. Stop and tell the operator; do not retry.429is the rate limit. Wait forRetry-Afterand retry.
Reseller API ​
Every operation above has a reseller-scoped equivalent under the /api/reseller prefix, so a reseller can connect their own billing platform without administrator credentials:
GET /api/reseller/health
GET /api/reseller/hosting-plans
GET /api/reseller/hosting-accounts?username=<username>
POST /api/reseller/hosting-accounts
PUT /api/reseller/hosting-accounts/{id}
DELETE /api/reseller/hosting-accounts/{id}
POST /api/reseller/hosting-accounts/{id}/suspend
POST /api/reseller/hosting-accounts/{id}/unsuspend
POST /api/reseller/hosting-accounts/{id}/change-password
GET /api/reseller/hosting-accounts/{id}/usage-details
POST /api/reseller/hosting-accounts/{id}/generate-sso-tokenRequest bodies, response shapes, and error semantics are identical. What differs:
- The key pair is a reseller key, issued in the reseller panel under Security > Api Keys. An administrator key on this prefix is refused with
403, and so is a reseller key on the administrator prefix. - Everything is scoped to that reseller. Listings and lookups return only the reseller's own accounts, and a plan named in a request has to be one of the reseller's plans.
reseller_idis implied. New accounts belong to the key's reseller; the field cannot be set or changed.
The AdminBolt modules for Blesta, HostBill, and Upmind expose a reseller credentials switch in their server configuration, which moves the module onto this prefix. One module therefore serves both administrators and resellers.
See Reseller Hosting Accounts in the reference for the endpoint details.
Troubleshooting ​
| Symptom | Cause and fix |
|---|---|
A call comes back as a 302 redirect, or as HTML | The request did not send Accept: application/json, so a validation failure was answered the way a browser would be answered. Add the header, and turn redirect following off in the HTTP client. |
Every call returns 403 IP address not whitelisted | The billing server reaches the panel from an address that is not on the key's whitelist, often because it egresses through a proxy or a second interface. Add the address the panel actually sees, or turn the whitelist off while you check. |
Test Connection succeeds but provisioning returns 403 | The key is restricted to a subset of endpoints. Either grant the account endpoints or allow all endpoints on the key. |
A plan name that exists returns Hosting plan not found | The plan belongs to a different owner. An administrator key reaches a reseller's plan only when the request also names that reseller, and a reseller key reaches only its own plans. |
Usage shows as 9 instead of 9999.95 | Something in the integration is parsing a formatted string. The API returns plain numbers; check that the value is not being passed through a display helper first. |
| A customer reports the log-in button worked once, then stopped | Single sign-on tokens are single-use and one is active per account. Generate a token per click. |
Writes return 403 while reads keep working | The panel license has expired. Renew it in the panel under Settings > License. |
Related pages ​
- API Reference: the generated endpoint reference, with every parameter and response field.
- API Keys: issuing, restricting, and revoking administrator keys.
- Reseller API Keys: the same for resellers.
- WHMCS: the ready-made WHMCS server module built on this API.