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

On this page

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:

Signature payload
<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:

HeaderValue
X-XDP-Keythe API key
X-XDP-Timestampunix seconds — rejected beyond ±5 minutes
X-XDP-Signature-Version1
X-XDP-Signaturehex HMAC-SHA256 of the payload
X-XDP-Nonceunique per request — replays inside the window are rejected
Signed request, end to end
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
TypeScript signer
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.

Next steps