# 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.
>
> Canonical: https://xdp.network/docs/api-reference/request-signing · Updated 2026-08-09

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:

```text title="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:

| 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 |

```bash title="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
```

```ts title="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.

:::warning
Sign the exact body bytes you send. Re-serializing JSON differently (key order, whitespace)
changes the body hash and fails verification.
:::

## 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

- [Authentication](/docs/api-reference/authentication)
- [Endpoints](/docs/api-reference/endpoints)
- [Errors](/docs/api-reference/errors)
