HTTP API and webhooks

Everything the admin panel does is available over the HTTP API: a billing system, monitoring or your own script runs the panel just like an administrator. Webhooks work the other way — the panel tells an external system about events itself. A billing system usually needs both. For WHMCS there's a ready module — WHMCS module.

API keys

An API key is a password for a program. With it an external system works in its own name, without a staff login or a 2FA code. Only an administrator issues keys: Access → API keys → New key, a name and a scope:

  • read — read only: nodes, plans, the catalog. Nothing can be changed with it. Such a key sees no game servers — a key has no owner, and servers belong to owners.
  • admin — full access to the whole API, like an administrator. A billing system needs this one. Give it only to systems you trust as much as an administrator.

The gmk_… key is shown once — the panel keeps only its hash. Lost it — revoke it and issue a new one.

The key goes in the header of every request:

curl -H "Authorization: Bearer gmk_…" https://panel.example.com/api/nodes

Tips:

  • Issue a separate key per system. The list then shows when each key was last used, the audit log records actions under its name, and revoking one key doesn't break the other integrations.
  • Keep the key in the system's settings or environment variables, not in code or a repository.
  • A key leaked — Revoke: it stops working at once. A revoked key stays in the list so the audit history isn't lost.
  • Each key is rate-limited: 20 requests per second by default, bursts up to 200. Over the limit the panel answers 429. The limit is changed in Settings.

Method reference

The full API reference is in the panel itself at /api/docs/ (Swagger): methods, fields, response codes. You can try requests there with your key.

Your own billing integration

There's no separate "billing API" — a billing system uses the regular methods with an admin key. A billing service is a game server with an external_id field (your service number); a billing client is a panel user with the customer role (viewer).

  • Create a service: GET /api/users?email=…; if there's no such client — POST /api/users with role: viewer; then POST /api/game-servers with plan_id, location_id, owner_user_id and external_id.
  • Find a service's server: GET /api/game-servers?external_id=….
  • Suspend / resume: POST /api/game-servers/{id}/suspend ({"reason": "…"}) / …/unsuspend.
  • Change plan: PATCH /api/game-servers/{id}/plan ({"plan_id": "…"}).
  • Delete a service: DELETE /api/game-servers/{id}.
  • Change a client's password: PATCH /api/users/{id} ({"password": "…"}) — their sessions end.
curl -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"plan_id":"plan_…","location_id":"loc_fra","owner_user_id":"…","external_id":"billing-1042"}' \
  https://panel.example.com/api/game-servers

external_id is unique: a repeated POST with the same number returns 409 and creates no second server. So the request is safe to retry after a timeout — find the server by external_id and carry on. The number can be set only at creation.

A server's resources change only through its plan: for a special case, create a separate plan and move the server to it.

Webhooks

A webhook is a POST request with JSON about an event, sent to your URL. Integrations → Webhooks → New webhook: the receiver's URL and the events. All events includes the ones added in future versions, so the receiver must quietly skip unknown types.

Main events:

  • server.created, server.started, server.stopped, server.deleted — what actually happened on the node (not the moment of the API call);
  • server.suspended, server.unsuspended — suspension and resumption;
  • server.failed — the server didn't come up after all attempts; the data carries the error text;
  • node.offline, node.online — a node went away or came back;
  • backup.completed, backup.failed;
  • schedule.run.completed, schedule.run.failed — schedule runs;
  • database.created, database.deleted — server databases (no password).

Server events carry external_id and connect_address — handy for finding the service and sending the customer the address to connect to.

The request body:

{"id": "evt_…", "type": "server.created", "created_at": "…", "data": {"id": "gs_…", "external_id": "billing-1042", "connect_address": "203.0.113.10:27015", "…": "…"}}

Verifying the signature

After a webhook is created the panel shows its secret once. Every request is signed with the header X-Webhook-Signature: t=<unix time>,v1=<hex>, where v1 is an HMAC-SHA256 of the string <t>.<request body> with that secret. Compute it over the raw body, before parsing the JSON, and reject requests with an old t:

sig = dict(p.split("=", 1) for p in request.headers["X-Webhook-Signature"].split(","))
expected = hmac.new(secret.encode(), f"{sig['t']}.".encode() + request.body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, sig["v1"]) and abs(time.time() - int(sig["t"])) < 300

Secret lost or leaked — Change secret; the old one stops working at once.

Retries and guarantees

  • A delivery succeeds only on a 2xx answer within 10 seconds. Redirects count as errors.
  • After an error the panel retries with a growing pause (30 seconds, a minute, two… up to an hour), up to 10 attempts. Then the delivery is marked failed, and Retry in the delivery log sends it again.
  • One event can arrive twice, and order isn't guaranteed. Drop duplicates by the event id (also in the X-Webhook-Id header) and don't rely on ordering — when in doubt, ask the API for the current state.
  • Send test event checks the receiver; the response code and error text show under Deliveries.