Authentication
Every request must carry HMAC-SHA256 authentication headers. The signed payload is canonicalised so a captured signature cannot be replayed against a different endpoint, body or window.
X-WHost-KeyAPI key id (configurable in /admin/api-keys).X-WHost-TimestampCurrent Unix timestamp (seconds).X-WHost-Nonce16–256 characters, unique per request (32 hex chars recommended); a replayed nonce is refused.X-WHost-SignatureHMAC-SHA256(api_secret, "METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY") hex digest.The agent rejects requests:
- with a timestamp older than 300 seconds,
- whose nonce was already seen inside the replay window (twice the 300 s timestamp tolerance plus 60 s),
- whose signature, recomputed server-side, doesn't match,
- whose path drifts from the signed one after
posixpath.normpathand matrix-param stripping (so trailing;foo=barcannot bypass the path binding).
Account-bound (reseller) keys
A key minted from the client panel by a reseller account (Settings → API Access) signs requests exactly like a server key, but is served as that reseller: it reaches only /api/v1/client/* and acts there with the reseller's ownership checks and ACL plan. What such a key gets back:
| Request | Answer |
|---|---|
Any path outside /api/v1/client/ |
403 SCOPE_DENIED |
/api/v1/client/api-keys*, /api/v1/client/profile* (credential and profile management) |
403 HMAC_FORBIDDEN_FOR_CREDENTIAL_MUTATION — browser session only |
| A sub-account the reseller does not own | 404 ACCOUNT_NOT_FOUND |
Reseller's ACL plan without api_access, or the account no longer a reseller |
403 PERMISSION_DENIED |
| Owner account suspended, terminated or deleted; key revoked | 401 AUTH_FAILED |
The key's scope is fixed (client) and cannot be widened; a reseller holds at most 10 keys. Management endpoints for the reseller's own keys: GET/POST /api/v1/client/api-keys, PUT /api/v1/client/api-keys/{key_id}, POST /api/v1/client/api-keys/{key_id}/revoke, DELETE /api/v1/client/api-keys/{key_id}, GET /api/v1/client/api-keys/logs — session cookie only. The administrator sees every key under GET /api/v1/system/api-keys; the owner field names the reseller (null for server-wide keys).
Canonical signing payload (v2)
METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY
PATH is the agent-visible path (the part after the host, including
the /api/v1/... prefix). BODY is the raw HTTP body bytes decoded
as UTF-8 with surrogateescape (binary uploads are signable without
re-encoding).
Reference Python signer
import hashlib, hmac, json, secrets, time
from urllib.request import Request, urlopen
import ssl
API_KEY = "your-api-key"
API_SECRET = "your-api-secret"
BASE_URL = "https://panel.example.com"
def sign(method: str, path: str, body: bytes = b"") -> dict[str, str]:
ts = str(int(time.time()))
nonce = secrets.token_hex(16)
body_str = body.decode("utf-8", errors="surrogateescape")
payload = f"{method}\n{path}\n{ts}\n{nonce}\n{body_str}"
sig = hmac.new(
API_SECRET.encode("utf-8"),
payload.encode("utf-8", errors="surrogateescape"),
hashlib.sha256,
).hexdigest()
return {
"X-WHost-Key": API_KEY,
"X-WHost-Timestamp": ts,
"X-WHost-Nonce": nonce,
"X-WHost-Signature": sig,
"Content-Type": "application/json",
}
body = json.dumps({"username": "alice"}).encode("utf-8")
headers = sign("POST", "/api/v1/accounts", body)
req = Request(f"{BASE_URL}/api/v1/accounts", data=body,
headers=headers, method="POST")
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE # remove in production
print(urlopen(req, context=ctx).read())
Signers in other languages live next to this file:
- PHP —
docs/developer/examples/hmac-example.php - JavaScript / Node.js —
docs/developer/examples/hmac-example.js - Go —
docs/developer/examples/hmac-example.go - Ruby —
docs/developer/examples/hmac-example.rb - Bash / POSIX sh —
docs/developer/examples/hmac-example.sh
Admin session cookie (browser-only)
The /admin/* panel and the /client/* panel mint an httpOnly
session cookie via POST /api/v1/auth/login (then /api/v1/auth/login/2fa
when 2FA is enabled). SDKs and partner integrations should always
use HMAC instead — sessions are scoped to a browser context and rotate
on every privilege change.
Our support team is here around the clock for anything you can't find above.