API reference
Read your servers, usage, orders, invoices and subscriptions; reboot a server, take and restore snapshots; and manage signed webhook endpoints, all from a script. Ordering, rebuilding, removing a server and revealing credentials stay in the dashboard. The API goes through the same rules and audit trail as the dashboard, so nothing is possible here that a person could not do by hand.
Authentication
Create a key under API keys in the dashboard. It is shown once. A key acts for your whole organization and its actions are recorded against the person who created it. Send it as a bearer token:
curl -H "Authorization: Bearer haas_..." https://datahall.cloud/api/v1/serversBase URL: https://datahall.cloud/api/v1. Every response is JSON and is never cached.
Rate limit
60 requests per key per 60 seconds. Over the limit you get 429 with a Retry-After header in seconds.
Errors
Every error has the same shape:
{ "error": { "status": 401, "message": "Unknown or revoked API key." } }- 401: missing, malformed, unknown or revoked key.
- 403: the key can read but not act, because the person who created it no longer exists.
- 429: rate limit exceeded.
Endpoints
GET/api/v1/orders
List every order the organization has placed, newest first.
Placing an order stays in the dashboard, where payment is collected. "heldForReview" is true while a human still has to look at an unpaid order; nothing can be paid until it is cleared.
Response 200
{
"orders": [
{
"id": "7f1c0a3e-2b6d-4c8e-9a1f-0d2e3c4b5a69",
"plan": "app-s",
"planName": "App S",
"hostname": "web-1",
"region": "us-east",
"operatingSystem": "ubuntu-24-04",
"status": "completed",
"heldForReview": false,
"billingInterval": "month",
"pricedAtCents": 2400,
"currency": "usd",
"addOns": ["monitoring"],
"supportTier": "standard",
"createdAt": "2026-09-14T20:15:53.000Z"
}
]
}GET/api/v1/invoices
List invoices, newest first: what Stripe reported and what we issued on terms.
Amounts are integer cents. "source" is "stripe" for card accounts (with a hosted invoice link) or "manual" for an account on terms (with our invoice number, a due date and a link to the printable document, which needs a signed-in dashboard session). The latest fifty.
Response 200
{
"invoices": [
{
"id": "5b0c9c6e-6b0a-4f2f-9c8e-2f7d1c3a4b5d",
"number": null,
"status": "paid",
"source": "stripe",
"amountDueCents": 2400,
"amountPaidCents": 2400,
"currency": "usd",
"dueAt": null,
"periodStart": "2026-09-01T00:00:00.000Z",
"periodEnd": "2026-10-01T00:00:00.000Z",
"paidAt": "2026-09-01T00:02:11.000Z",
"hostedInvoiceUrl": "https://invoice.stripe.com/i/example",
"documentUrl": null,
"createdAt": "2026-09-01T00:00:05.000Z"
}
]
}GET/api/v1/subscriptions
List subscriptions, one per server on a card account.
"suspendsAt" is set while a subscription is past due: the date the server is suspended if still unpaid. Accounts on terms have invoices, not subscriptions.
Response 200
{
"subscriptions": [
{
"id": "9e2a7f1c-0d4b-4c3a-8e5f-6a7b8c9d0e1f",
"hostname": "web-1",
"status": "active",
"currentPeriodEnd": "2026-10-01T00:00:00.000Z",
"cancelAtPeriodEnd": false,
"suspendsAt": null
}
]
}GET/api/v1/servers
List every server on the organization the key belongs to.
Response 200
{ "servers": [ {
"id": "3c648141-ae58-4e1c-846a-1f57b8680d52",
"hostname": "web-1",
"plan": "app-s",
"region": "us-east",
"operatingSystem": "ubuntu-24-04",
"status": "active",
"statusText": "Running",
"ipv4": "203.0.113.42",
"ipv6": null,
"createdAt": "2026-09-14T20:15:53.000Z"
} ] }GET/api/v1/servers/{id}
One server.
A server on another organization is indistinguishable from one that does not exist.
Response 200
{ "server": {
"id": "3c648141-ae58-4e1c-846a-1f57b8680d52",
"hostname": "web-1",
"plan": "app-s",
"region": "us-east",
"operatingSystem": "ubuntu-24-04",
"status": "active",
"statusText": "Running",
"ipv4": "203.0.113.42",
"ipv6": null,
"createdAt": "2026-09-14T20:15:53.000Z"
} }- 404: Unknown id, or not yours.
GET/api/v1/servers/{id}/usage
The last 24 hours of usage for one server.
Only servers on providers we read metrics from have usage; a hand-built server answers 404 until it does. Rates are bytes per second, CPU is a percentage of all cores, and the series run oldest first with null for a missed collection. "month" is the UTC calendar month to date against the plan's included outbound transfer; "level" is "ok", "nearing" (from 80%) or "exceeded". Exceeding it never changes the server or adds a charge by itself.
Response 200
{
"usage": {
"hours": 24,
"windowMinutes": 15,
"samples": 96,
"latest": {
"sampledAt": "2026-09-14T20:15:00.000Z",
"cpuPercent": 12.5,
"inboundBytesPerSec": 48000,
"outboundBytesPerSec": 21000
},
"averageCpuPercent": 9.8,
"peakCpuPercent": 41,
"transferredBytes": 5961600000,
"series": { "cpuPercent": [8, 9, null, 12.5], "networkBytesPerSec": [61000, 66000, null, 69000] },
"month": {
"month": "2026-09",
"outboundBytes": 812000000000,
"inboundBytes": 1900000000000,
"allowanceBytes": 4000000000000,
"level": "ok"
}
}
}- 404: Unknown id, not yours, or no samples yet.
POST/api/v1/servers/{id}/reboot
Ask for a reboot.
Same rules as the dashboard: only a running server can be rebooted, and the request is audited against the person who issued the key. Poll the server until its status returns to active.
Response 202
{ "ok": true, "status": "rebooting" }- 404: Unknown id, or not yours.
- 409: The server is not in a state that can be rebooted.
GET/api/v1/servers/{id}/snapshots
List snapshots of a server.
Response 200
{
"snapshots": [
{
"id": "a96f7ddd-0c3e-4b5a-9c6a-1d0f7d5d1e2f",
"name": "before-upgrade",
"status": "available",
"sizeGb": 20,
"createdAt": "2026-09-14T20:16:15.000Z",
"completedAt": "2026-09-14T20:16:40.000Z"
}
]
}- 404: Unknown id, or not yours.
POST/api/v1/servers/{id}/snapshots
Take a snapshot.
Snapshots settle asynchronously: the response is 202 and the snapshot appears as pending until the provider finishes. Each server keeps a limited number; delete old ones from the dashboard first.
Request body
{ "name": "before-upgrade" }Response 202
{ "ok": true, "status": "pending" }- 400: Body is not JSON or has no "name".
- 404: Unknown id, or not yours.
- 409: Not allowed for this server right now, or snapshots are not available on it.
POST/api/v1/servers/{id}/snapshots/{snapshotId}/restore
Restore a server from one of its snapshots.
Everything on the disk is replaced, so the hostname must be retyped in the body, exactly as in the dashboard. The snapshot must be available and belong to the server in the path. The server reads as provisioning until the restore settles.
Request body
{ "confirmHostname": "web-1" }Response 202
{ "ok": true, "status": "provisioning" }- 400: Body is not JSON or has no "confirmHostname".
- 404: Unknown snapshot, not on this server, or not yours.
- 409: The hostname does not match, the snapshot or server is not ready, or restores are not available on it.
GET/api/v1/webhooks
List webhook endpoints.
Response 200
{
"webhooks": [
{
"id": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"url": "https://example.com/hooks/datahall",
"description": "Ops channel bot",
"enabled": true,
"consecutiveFailures": 0,
"disabledAt": null,
"createdAt": "2026-09-14T20:15:53.000Z"
}
]
}POST/api/v1/webhooks
Add a webhook endpoint.
The person who issued the key must be an owner of the organization, as in the dashboard. https on a public hostname only, up to five endpoints. The signing secret is in this response and nowhere else: it is derived, not stored, so it cannot be shown again. See the Webhooks section below for the signature and events.
Request body
{ "url": "https://example.com/hooks/datahall", "description": "Ops channel bot" }Response 201
{ "id": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f", "secret": "whsec_…" }- 400: Body is not JSON, has no "url", or the URL is refused.
- 403: The key was issued by someone who is not an owner.
DELETE/api/v1/webhooks/{id}
Remove a webhook endpoint.
Owner only. Pending deliveries to it are dropped. Add it again for a fresh secret.
Response 200
{ "ok": true }- 403: The key was issued by someone who is not an owner.
- 404: Unknown id, or not yours.
Not in the API
Ordering, destroying, rebuilding and revealing credentials stay in the dashboard on purpose: each needs a person to confirm something a script should not.
Webhooks
Register an https endpoint under Webhooks in the dashboard and we POST a JSON body for each event, retrying with growing gaps for about a day if your endpoint does not answer with a 2xx. Each request carries x-datahall-signature: t=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256 of <t>.<raw body> under your endpoint’s secret. Refuse a signature whose timestamp is more than 300 seconds old, and compare in constant time.
{
"id": "delivery id",
"event": "server.active",
"occurredAt": "2026-09-14T20:15:53.000Z",
"data": { "serverId": "…", "hostname": "web-1", "ipv4": "203.0.113.42" }
}// Node
import { createHmac, timingSafeEqual } from 'node:crypto'
const [t, v1] = header.split(',').map((p) => p.split('=')[1])
const mac = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
const ok = timingSafeEqual(Buffer.from(mac, 'hex'), Buffer.from(v1, 'hex'))
&& Math.abs(Date.now() / 1000 - Number(t)) < 300- ping
- Sent when you ask for a test from the dashboard.
- server.active
- A server finished building or rebuilding and is running.
- server.suspended
- A server was suspended.
- server.resumed
- A suspended server was resumed.
- monitoring.down
- A monitored server stopped answering.
- monitoring.recovered
- A monitored server answered again.
- snapshot.available
- A snapshot finished and can be restored.
- invoice.paid
- An invoice was paid.
- payment.failed
- A payment failed; the grace period has started.