Request signing
How to sign console API requests with an API key: the XDP1 scheme — HMAC-SHA256 over timestamp, method, path and body, with a 5-minute freshness window and replay protection.
Last updated
Every programmatic call to the console API must be signed with an API key. This page shows the XDP1 scheme and gives you working code to copy. Signing stops credential replay, request tampering, and key-guessing dead.
Create a key
Settings → Security → API keys → Create key. Keys are shown once; store only the hash on
our side. A key looks like xdpk_0123abcd… — treat it like a password.
The XDP1 scheme
Compute an HMAC-SHA256 over four lines, using the key itself as the secret:
<unix-timestamp-seconds>
<METHOD>
<path> e.g. /api/servers (no query string)
<sha256-hex(body)> empty body hashes as the empty string
Send these headers:
| Header | Value |
|---|---|
X-XDP-Key | the API key |
X-XDP-Timestamp | unix seconds — rejected beyond ±5 minutes |
X-XDP-Signature-Version | 1 |
X-XDP-Signature | hex HMAC-SHA256 of the payload |
X-XDP-Nonce | unique per request — replays inside the window are rejected |
KEY="xdpk_…"
TS=$(date +%s)
BODY=''
NONCE=$(cat /proc/sys/kernel/random/uuid)
PAYLOAD=$(printf '%s\n%s\n%s\n%s' "$TS" "GET" "/api/servers" "$(printf '%s' "$BODY" | sha256sum | cut -d' ' -f1)")
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$KEY" | cut -d' ' -f2)
curl -H "X-XDP-Key: $KEY" -H "X-XDP-Timestamp: $TS" \
-H "X-XDP-Signature-Version: 1" -H "X-XDP-Nonce: $NONCE" \
-H "X-XDP-Signature: $SIG" https://xdp.network/api/servers
import crypto from "node:crypto";
export function signedHeaders(key: string, method: string, path: string, body = "") {
const ts = String(Math.floor(Date.now() / 1000));
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const payload = [ts, method.toUpperCase(), path, bodyHash].join("\n");
return {
"X-XDP-Key": key,
"X-XDP-Timestamp": ts,
"X-XDP-Signature-Version": "1",
"X-XDP-Nonce": crypto.randomUUID(),
"X-XDP-Signature": crypto.createHmac("sha256", key).update(payload).digest("hex"),
};
}
Failure responses
401 with a reason: stale / future (clock outside the window), bad_signature (body or
payload mismatch), replay (same timestamp+nonce twice), bad_version, missing.
Signed-read failures also appear in the security event stream.
Which routes accept signed keys
Read endpoints: GET /api/servers, GET /api/servers/:id, GET /api/products,
GET /api/billing/summary, GET /api/account/me. Reads and writes: /api/tickets.
Session-cookie auth keeps working from the dashboard; keys are for scripts and automation.
Ask via a ticket if you need another route signed.