Developer Portal
This is the single entry point for integrating with WHost. Pick the path that matches what you're trying to build:
examples/03-billing-integration-pattern.php in the SDK archiveapi-reference-generated.md (629 operations)versioning-policy.mdQuick Start
Every WHost API request needs four HMAC headers. Below is the canonical
payload format and a one-shot cURL probe — once it returns 200, you
have a working integration foundation.
The API is served under the panel's own address:
https://<panel-address>/api/v1/…. The agent itself listens only on
127.0.0.1:2000; the panel's web server forwards /api/v1/ requests to it.
1. Get credentials
In the admin panel:
Settings (gear icon in the top bar) → API Access → Create API Key
Pick the Full Access scope for a general integration — an Audit
Read-Only key reaches only /api/v1/audit-logs. Copy the API Key AND
the API Secret immediately — the secret is shown once. Restrict the
key with Allowed IPs (one IP or CIDR per line) if the consuming server
has a stable address.
2. Compute v2 HMAC signature
Canonical payload — every component newline-separated:
METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY
METHOD— uppercase HTTP verb (GET,POST, …)PATH— the request path exactly as sent, including the/api/v1/...prefix: not URL-decoded, and followed by?and the query string when the request has one (/api/v1/accounts?limit=10)TIMESTAMP— current Unix time, seconds (string); must be within 300 s of the server clockNONCE— 16–256 characters, unique per request (the examples use 32 hex characters); a nonce already seen in the last 11 minutes is refusedBODY— the exact request body bytes (empty for a request without a body)
X-WHost-Signature carries the lowercase hex HMAC-SHA256 of this payload,
keyed with the API secret.
Working signers in five languages — each one signs
GET /api/v1/system/info:
| Language | File |
|---|---|
| PHP | examples/hmac-example.php |
| JavaScript / Node.js | examples/hmac-example.js |
| Go | examples/hmac-example.go |
| Ruby | examples/hmac-example.rb |
| Bash / POSIX sh | examples/hmac-example.sh |
3. First request
# Using the Bash signer from the table above:
export WHOST_BASE_URL="https://panel.example.com" # the panel address
export WHOST_API_KEY="..."
export WHOST_API_SECRET="..."
bash hmac-example.sh
# Expected: the system/info envelope — {"status":"success","data":{…}}
If the request is refused, the error_code in the response body names the
check that refused it (the Bash signer runs curl with -f, which hides the
body — the PHP, Node.js, Go and Ruby signers print the status and body):
401 AUTH_EXPIRED— clock drift:X-WHost-Timestampis more than 300 s away from the server clock. Runtimedatectl statuson the calling host.401 AUTH_FAILED— the key, the signature or the nonce was refused. The most common causes:- Body byte mismatch — sign exactly the bytes you send; a proxy on your side that recompresses or re-encodes the request body breaks the signature.
- Wrong
PATH— it must start with/api/v1, keep the exact case and any percent-encoding, and include the query string when there is one. - A nonce shorter than 16 characters, or one already used.
- The signature sent in uppercase hex.
403 IP_NOT_ALLOWED— the calling address is not in the key's Allowed IPs.403 SCOPE_DENIED— the key's scope does not cover the path.429 AUTH_RATE_LIMITED— 30 failed verifications from one address within 5 minutes block that address for an hour.
4. Make it idempotent
Every mutating call (POST / PUT / PATCH / DELETE) accepts an
optional X-Idempotency-Key header — 8–64 characters of A–Z a–z 0–9 _ -
(anything else → 400 IDEMPOTENCY_KEY_INVALID). Use a UUID v4 per logical
operation:
curl -X POST … \
-H "X-Idempotency-Key: $(uuidgen)" \
-d '{"username":"alice","password":"<account password>","domain":"alice.example.com","email":"[email protected]","plan_id":"starter"}' \
"$WHOST_BASE_URL/api/v1/accounts"
(… stands for -H "Content-Type: application/json" and the four
X-WHost-* headers from step 2, signed over the exact body string.)
Same key + same body within 24 h → the stored response is replayed
(X-Idempotent-Replay: true). 2xx and 4xx answers are stored; a 5xx answer
is not, so a retry after a 5xx runs the call again.
Same key + a different method, path or body → 409 IDEMPOTENCY_KEY_REUSED
(caller bug). A key belongs to the API key (or session) that sent it: another
credential sending the same key runs a new request and never receives your
stored answer.
Full semantics: api-reference.md → Idempotency.
5. Subscribe to lifecycle events
Polling for state changes? Don't — register a webhook:
curl -X POST … \
-d '{
"url": "https://your-system.example.com/whost-hook",
"events": ["account.created","account.suspended","account.terminated","ssl.failed"],
"description": "billing platform"
}' \
"$WHOST_BASE_URL/api/v1/system/webhooks"
Save the returned secret — it's used to HMAC-verify inbound payloads.
Full subscriber recipe (PHP SDK helper, signature formula, retry semantics,
SSRF guards): webhooks.md.
Reference documents
| Topic | Document |
|---|---|
| Authentication, idempotency, pagination, rate limits, error catalog | api-reference.md |
| Full per-endpoint schemas (629 operations) | api-reference-generated.md |
| Webhook event catalog (25 events), payload, headers, signing, retry | webhooks.md |
Version stability promise (no breaking change inside /api/v1; at least 15 months from a /api/v2 announcement to v1 removal) |
versioning-policy.md |
| PHP SDK: download, install, what it covers | sdk-php.md |
| Python app hosting (Django, Flask, FastAPI runtime pipeline) | python-apps.md |
| HMAC signer examples (5 languages) | Quick Start → step 2 |
Common integration patterns
Pattern A — billing platform provisioning hook
For WISECP, WHMCS, Blesta and custom billing systems. Plain-PHP
reference: examples/03-billing-integration-pattern.php in the SDK archive.
order_activated → POST /api/v1/accounts (idempotency key: "order-create-$id")
order_suspended → POST /api/v1/accounts/{u}/suspend (key: "order-suspend-$id")
order_resumed → POST /api/v1/accounts/{u}/unsuspend (key: "order-resume-$id")
order_cancelled → DELETE /api/v1/accounts/{u} (key: "order-terminate-$id")
package_changed → POST /api/v1/accounts/{u}/change-package (key: "pkg-change-$changeId")
Idempotency keys mean network retries on timeout are safe — same logical order will not double-provision.
Pattern B — out-of-band billing reconciliation
Subscribe to the account. events (name each one — the events filter
takes exact names, not wildcards); your billing system stays in sync when
an operator manually suspends / re-activates via the panel.
account.suspended → mark order "suspended" in billing
account.unsuspended → flip back to "active"
account.terminated → reconcile cancellation
account.package_changed → recompute pricing
account.password_changed → audit log entry only
Pattern C — SSL certificate events
ssl.failed → page on-call + open support ticket
ssl.renewed → silence the open ticket (if any)
ssl.issued → record the new validity window (data.extra.valid_to)
These events come from certificate requests and uploads made through the
panel or the API. Renewals that certbot runs on its own schedule send no
event, and not every automatic certificate request does (see the
event catalog); to watch expiry, poll
GET /api/v1/accounts/{username}/ssl/{domain} (valid_to).
Pattern D — backup verification
backup.completed → record success + size + timing (data.extra.size_mb)
backup.failed → page on-call (operator's responsibility, not customer's)
A backup whose archive was created but whose remote upload failed arrives
as backup.completed with data.extra.remote_upload_error set.
Pattern E — capacity planning via metrics
The /api/v1/system/info endpoint returns CPU / memory / disk usage.
Poll on a 5-minute cadence (within the read tier rate limit) and trend
in your own observability stack; GET /api/v1/system/metrics?period=24h
(24h, 7d or 30d) returns the history the agent samples every 15
minutes. WHost itself doesn't expose Prometheus metrics today.
Stability & deprecation
/api/v1/* is stable — no breaking change ships under this prefix.
Backwards-incompatible work goes through /api/v2/, served in parallel
with v1: at least 15 months pass between the v2 announcement and the
removal of v1 (a 3-month beta plus a deprecation window of at least 12
months).
This means an integration written today against v1 keeps working
unchanged until v1's announced sunset date. Read
versioning-policy.md for the full deprecation
timeline, header conventions, and how Sunset headers will surface when v2
is introduced.
Support
CHANGELOG.md, PHP SDK page) — the main channel[email protected]The same, when you cannot open a ticket[email protected] — responsible disclosure expectedWhen reporting an integration bug, please include:
- The full request (headers + body, with secrets redacted)
- The full response (status, headers, body)
- The agent version (
GET /api/v1/system/update/status→data.current_version) - The SDK version, if using one
Our support team is here around the clock for anything you can't find above.