Skip to content

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:

PlatformModuleDownloadPanel
WHMCSAdminBolt for WHMCSbolt-whmcs.zipAny supported version
BlestaAdminBolt for Blesta (bolt-blesta)bolt-blesta.zip1.8.2 or newer
HostBillAdminBolt for HostBill (bolt-hostbill)Not released yet1.8.2 or newer
UpmindAdminBolt for Upmind (bolt-upmind)bolt-upmind.zip1.8.2 or newer

Each package is a zip with one folder named after the module; the README inside it has the install steps for that platform. The links always serve the latest release, and every release is also kept at https://get.adminbolt.com/downloads/<module>/v<version>/<module>-<version>.zip, where <module> is the package name, for example https://get.adminbolt.com/downloads/bolt-blesta/v1.0.0/bolt-blesta-1.0.0.zip.

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 by hosting_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/api

Before 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/json on every request as well. Without it, a request that fails field validation is answered with a 302 redirect and an HTML body instead of a 422 and 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. Set Accept, 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:

StatuserrorCause
401API key is missing / API secret is missingA header was not sent.
401Invalid API key or secretThe pair does not match a key.
401API key is not activeThe key exists but is disabled.
403API key does not have access to this resource.An administrator key was used on the reseller API, or the reverse.
403IP address not whitelistedThe calling address is not on the key's whitelist.
403API key does not have access to this endpoint or methodThe 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: 36

Beyond 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:

bash
curl -s https://panel.example.com:8443/api/health \
  -H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
  -H "Accept: application/json"
json
{ "status": "ok" }

Hosting plans ​

A billing product maps onto an AdminBolt hosting plan. List the plans to populate the product configuration dropdown:

bash
curl -s https://panel.example.com:8443/api/hosting-plans \
  -H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
  -H "Accept: application/json"
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:

json
{ "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 ​

bash
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" }'
FieldRequiredNotes
domainYesThe account's main domain. It is the account's identity and cannot be changed later.
hosting_planOne of the twoExact plan name.
hosting_plan_idOne of the twoNumeric plan id.
usernameNoDerived from the domain when omitted.
passwordNoGenerated when omitted, and returned in the response.
reseller_idNoCreates the account under a reseller. It cannot be changed later.
ssh_accessNoDefaults to false.

A successful call returns 201:

json
{
  "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 ​

bash
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:

json
{ "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 ​

bash
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:

bash
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.

The account's identity fields 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:

json
{ "message": "domain cannot be changed via API." }

Both refusals return 422. On the administrator API, domain and username are refused when they differ from the current value. On the reseller API, domain and reseller_id are. Send the current value, or leave the field out.

Edit the rest of the account ​

Since panel 1.9.4, PUT /api/hosting-accounts/{id} on the administrator API changes the same fields as Edit Account in the admin panel, through the same checks:

  • reseller_id moves the account to another reseller, and null gives it to the administrator. The plan may stay as it is, as in the admin form.
  • bandwidth_limit overrides the plan's bandwidth. null uses the plan's value, and 0 is unlimited.
  • open_basedir_paths sets extra open_basedir paths, one per line. The answer reports, for each domain, whether its site configuration was rebuilt with them.
  • On CloudLinux, the LVE overrides (resource_cpu_speed, resource_physical_memory, and the other resource_* fields) and cagefs_enabled. The reseller's LVE pool still caps the overrides.
  • The contact fields: name, email, phone, address, city, state, zip, country, and company.

Every field is applied or refused by name. An unknown field, a wrong type, a CloudLinux field on a server without CloudLinux, or a body that is not valid JSON answers 422 with the refused fields in fields, and nothing is applied. When the change is saved but a step on the server is not applied, the answer carries warning, next_step, and correlation_id.

Two actions match buttons of the account page:

Method and pathWhat it does
POST /api/hosting-accounts/{id}/refresh-usageMeasures the account again, like Refresh Usage, and returns the stored counters. When the home folder cannot be measured, the last known disk usage is kept, marked stale, and the answer carries a warning.
DELETE /api/hosting-accounts/{id}/migrated-copyRemoves this server's copy of an account that moved to another node, like Remove the copy on this server. Refused, with the reason, until the move completed and, when data was copied, its data check passed. The account on the other node is not touched.

Suspend and unsuspend ​

bash
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 ​

bash
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 ​

bash
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"
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.95 is 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 0 means unlimited. Map it to whatever your platform uses for no limit, and never to a quota of zero.
  • diskUsageWarning is a boolean. It is true only when usage exceeds the plan allocation, and it is never true on an unlimited plan.

Since panel 1.9.0 the response also carries:

  • combinedDiskUsage - files and databases together, in MB, the figure measured against the plan's Disk Space.
  • diskUsagePercent - that combined figure as a percentage of the plan's Disk Space, and empty (null) on an unlimited plan.
  • diskUsageLevel - the state of the combined figure as the client dashboard shows it: normal, warning from 80%, or critical from 95%, and empty on an unlimited plan.
  • inodes and totalInodes - the files and folders the account holds, and the plan's limit. 0 means unlimited.
  • inodeUsageWarning - a boolean that is true from 90% of the plan's file limit.

The same fields are returned by the reseller and client usage-details endpoints. The client endpoint returns the same shape as the admin one: sizes and counts as plain numbers, and warnings as true or false.

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 ​

bash
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"
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 ​

OperationRepeated callAccount does not exist
Create422, the domain is already takenNot applicable
Look upSame resultEmpty array
Change passwordApplies again, 200404
Change packageApplies again, 200404
SuspendState re-applied, 200404
UnsuspendState re-applied, 200404
Terminate404, treat as already terminated404

Rules for module authors:

  • 2xx is success. The message field is written for a person and is safe to show to the operator.
  • 422 carries a readable message describing what was rejected. Surface it in the billing admin interface instead of a generic failure.
  • 404 on 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.
  • 401 and 403 are credential or whitelist problems. Stop and tell the operator; do not retry.
  • 429 is the rate limit. Wait for Retry-After and 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-token

Request 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_id is implied. New accounts belong to the key's reseller; the field cannot be set or changed.
  • The secret is shown once. Since panel 1.9.4, the reseller panel shows a new key's secret once, in a notification that stays until it is closed. Copy it then. See Reseller API Keys.

Beyond billing ​

Since panel 1.9.4, the reseller API also covers the rest of the reseller panel, on the reseller's own accounts, addresses, and templates only. Anything else answers 404, as if it did not exist.

Method and pathWhat it does
POST /api/reseller/hosting-accounts/{id}/refresh-usageMeasure the account again and return the stored counters, like Refresh Usage.
POST /api/reseller/hosting-accounts/{id}/primary-domainRename the account's primary domain, like Change primary domain. A domain that carries mail is refused, as in the panel.
GET, PATCH /api/reseller/settingsThe nameserver source, the two custom nameservers, and the default IP address of Webhost Manager and Network IP Settings.
GET, POST /api/reseller/landing-templates
GET, PATCH, DELETE /api/reseller/landing-templates/{id}
The templates of Landing Pages.
GET /api/reseller/network-ips
GET /api/reseller/network-ips/{id}
The IP addresses the administrator gave the reseller.
GET, POST /api/reseller/ip-assignments
GET, PATCH, DELETE /api/reseller/ip-assignments/{id}
The assignments of IP Assignments. PATCH changes only the notes.
GET /api/reseller/reports/dashboardThe figures of the reseller Dashboard and the usage of each reseller limit.
GET /api/reseller/reports/lve-usage
GET /api/reseller/reports/lve-faults
The figures of LVE Usage and LVE Faults. Without CloudLinux they answer 409.
GET /api/reseller/activity-logThe reseller's Activity Log.
/api/reseller/hosting-plan-feature-listsThe reseller's Feature Lists.

The checks are the reseller panel's:

  • Custom nameservers must start with ns, have at least three labels, and be two different names. With the root source, the nameservers are cleared and may not be sent.
  • The default IP address and an assigned address must be active addresses the administrator gave the reseller. An address serves one hosting account at a time, and a taken address or account answers 409.
  • Assigning or removing an address points the account's DNS records and websites at the new address, as on the IP Assignments page.
  • Resetting the two-factor authentication of an account owner stays in the reseller panel.

The AdminBolt modules for Blesta and Upmind (and HostBill, once released) 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 ​

SymptomCause and fix
A call comes back as a 302 redirect, or as HTMLThe 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 whitelistedThe 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 403The 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 foundThe 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.95Something 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 stoppedSingle sign-on tokens are single-use and one is active per account. Generate a token per click.
Writes return 403 while reads keep workingThe panel license has expired. Renew it in the panel under Settings > License.
  • 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.