# Provisioning API

> Create, suspend and bill hosting accounts from your own system with the /api/v1 provisioning API: tokens, endpoints, idempotency, rate limits and examples.

Source: https://zopanel.net/docs/provisioning-api  
Updated: 2026-10-07

The provisioning API at `/api/v1` lets a billing system or your own CMS manage hosting accounts on a ZoPanel server. The WHMCS, Blesta, HostBill and Paymenter modules all use it. Accounts are addressed by username and packages by name, so your system does not need to store any ZoPanel IDs. A machine-readable description is available as an OpenAPI 3 file (`docs/provisioning-openapi.yaml` in the source).

## Authentication

1. In ZoPanel, open **My account → API tokens** and click **Create token**.
2. Set **Permissions** to **Provisioning only (WHMCS, CMS)**.
3. In **Allowed from IPs**, enter the addresses of the servers that call the API (up to 20 IPs or ranges).
4. Copy the `zpat_…` token. It is shown only once.

Send the token in every request:

```http
Authorization: Bearer zpat_xxxxxxxxxxxx
```

API tokens require a Pro license. Keep in mind:

- A **Provisioning only** token can only call `/api/v1`. Anything else returns `403`.
- A call from an address that is not in the token's list returns `403`.
- If you restrict panel access under **Settings → General → Restrict panel access**, the calling server must be in that list too.
- If the panel requires two-factor authentication for the token's owner, the owner must have 2FA set up, or the token is refused.
- Changing the owner's password revokes all of the owner's tokens.

### Who owns the token

| Token owner | What the token can do |
| --- | --- |
| Administrator | Sells the administrator's packages and sees every hosting account |
| Reseller | Sells only the reseller's own packages and sees only the reseller's customers, within the reseller's plan |

## Endpoints

Base URL: `https://panel.example.com:8888/api/v1`

| Method and path | Purpose |
| --- | --- |
| `GET /api/v1` | API version, token owner and panel URL (connection test) |
| `GET /api/v1/packages` | Packages you can sell |
| `PUT /api/v1/packages/{name}` | Create or update a package by name |
| `POST /api/v1/accounts` | Create an account, optionally with its first website |
| `GET /api/v1/accounts/{username}` | Status, package and usage |
| `POST /api/v1/accounts/{username}/suspend` | Suspend (repeating it is harmless) |
| `POST /api/v1/accounts/{username}/unsuspend` | Unsuspend (repeating it is harmless) |
| `PUT /api/v1/accounts/{username}/package` | Change the package |
| `PUT /api/v1/accounts/{username}/password` | Set the panel and SFTP password |
| `POST /api/v1/accounts/{username}/login` | One-time sign-in link, valid once for 60 seconds |
| `DELETE /api/v1/accounts/{username}` | Terminate the account |
| `GET /api/v1/usage` | Usage of every account you manage |

Usernames are 3–16 characters, lowercase letters and digits, starting with a letter. Names starting with `zp` are reserved. Passwords are 8–128 characters.

### Notes on specific calls

- **Create:** `username`, `password` and `package` (name or ID) are required. `email`, `domain` and `php` are optional. The response holds `account` and either `site` or `site_error`. If the account was created but the website was not (for example, the domain is already hosted elsewhere), the order still succeeds and the customer can add the website later.
- **Login:** returns `url`, `path` and `expires_in`. It only works for active customer accounts. Resellers sign in with their own password and second factor.
- **Terminate:** the account's websites, databases, mail and files are removed. The data is kept on the server for the grace period set in **Settings → General → Keep deleted accounts (days)**.
- **Packages:** fields you do not send are set to 0. For most limits, 0 means unlimited. Exceptions: for `io_mbps` and `db_connections`, 0 means automatic and -1 means unlimited. `php_memory_mb` defaults to 256 and `php_children` to 10.

### Account and usage fields

`GET /accounts/{username}` and each item of `GET /usage` return:

| Field | Meaning |
| --- | --- |
| `username`, `email`, `package` | Account details |
| `status` | `active` or `suspended` |
| `created_at` | Unix time |
| `usage.disk_mb`, `usage.disk_limit_mb` | Disk used and the package limit (0 = unlimited), refreshed hourly |
| `usage.bandwidth_mb` | Traffic served in the current calendar month, from the access logs, refreshed hourly |
| `usage.sites`, `usage.sites_limit` | Websites |
| `usage.databases`, `usage.databases_limit` | Databases |

## Errors

Errors return a JSON body `{"error": "…"}` with a matching status code:

| Status | Meaning |
| --- | --- |
| 400 | Invalid input, for example an unknown package |
| 401 | Missing, invalid or expired token |
| 402 | License limit reached, or no Pro license |
| 403 | The token is not allowed (scope, IP address or 2FA policy) |
| 404 | Unknown account, or one you do not manage |
| 409 | A request with the same idempotency key is still running |
| 429 | Too many requests |

## Idempotency keys

Send a unique `Idempotency-Key` header with `POST`, `PUT` and `DELETE` requests, for example `billing-create-<order id>`. Keys can be up to 200 characters.

- If the same token repeats the same method and URL with the same key within 24 hours, it gets the first answer back, with the header `Idempotent-Replayed: true`. The action is not done again, so a retry after a timeout never creates an account twice.
- While the first request is still running, a repeat gets `409`.
- Server errors (`5xx`) and `429` answers are not saved, so those requests can be retried with the same key.

## Rate limits

Each token may make 1200 requests per minute. Above that, the API returns `429` with `Retry-After: 60`. Repeated failed authentication from one address is also throttled.

## Examples

```bash
T="Authorization: Bearer zpat_xxx"
J="Content-Type: application/json"
P=https://panel.example.com:8888/api/v1

# Connection test
curl -H "$T" $P

# Push a plan
curl -X PUT -H "$T" -H "$J" \
  -d '{"max_sites":3,"disk_mb":10240,"memory_mb":1024,"cpu_percent":100,"sftp":true}' \
  $P/packages/Starter

# Sell it (the website gets SSL once the domain points to the server)
curl -X POST -H "$T" -H "$J" -H "Idempotency-Key: order-1001" \
  -d '{"username":"shop1","password":"S3cure-Pass-1","email":"owner@shop1.vn","package":"Starter","domain":"shop1.vn","php":"8.3"}' \
  $P/accounts

# Invoice overdue, then paid
curl -X POST -H "$T" $P/accounts/shop1/suspend
curl -X POST -H "$T" $P/accounts/shop1/unsuspend

# Upgrade
curl -X PUT -H "$T" -H "$J" -d '{"package":"Business"}' $P/accounts/shop1/package

# "Login to control panel" button: redirect the customer to .url
curl -X POST -H "$T" $P/accounts/shop1/login

# Usage for billing
curl -H "$T" $P/usage
```

## Package fields

| Field | Meaning |
| --- | --- |
| `max_sites`, `max_databases`, `max_cron`, `max_ftp`, `max_mailboxes` | Counts (0 = unlimited) |
| `disk_mb`, `memory_mb` | Disk space and memory (0 = unlimited) |
| `cpu_percent` | CPU, where 100 = one core (0 = unlimited) |
| `processes` | Processes (0 = unlimited) |
| `io_mbps` | Disk throughput (0 = automatic, -1 = unlimited) |
| `db_connections` | Database connections per user (0 = automatic, -1 = unlimited) |
| `php_memory_mb`, `php_children` | PHP memory limit and workers |
| `sftp`, `terminal` | Access, `true` or `false` |
