> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omnibook.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Sign requests with an API key using HMAC-SHA256.

Every request to the Omnibook API is authenticated. There is no anonymous
access — even market data requires a key.

You authenticate by signing each request with your API key's secret and sending
three headers. Nothing is sent as a bearer token, so intercepting a request does
not give anyone your key.

## Create an API key

1. Sign in at [omnibook.xyz](https://omnibook.xyz).
2. Go to **Settings → API keys** (`/settings#keys`).
3. Click **Create key**, give it a name, and copy both values.

<Warning>
  **The secret is shown once and never again.** Store it somewhere safe before you
  close the panel. Copy the **Key ID** too — you need both, and the list view does
  not show the secret again.

  If you lose a secret, revoke the key and create a new one. Revoking is immediate
  and irreversible.
</Warning>

## The three headers

| Header                | Value                                             |
| --------------------- | ------------------------------------------------- |
| `DX-ACCESS-KEY`       | Your key ID, in decimal                           |
| `DX-ACCESS-TIMESTAMP` | Current Unix time in **milliseconds**, in decimal |
| `DX-ACCESS-SIGNATURE` | `base64(HMAC-SHA256(secret, canonical))`          |

## The canonical string

The signature covers the timestamp, method, request target and body, joined by
newlines, with a trailing newline after the target:

```
<timestamp> \n <METHOD> \n <request-target> \n <body>
```

Concretely, for `GET /v1/portfolio/balance` with no body:

```
1785299414489\nGET\n/v1/portfolio/balance\n
```

Three things that catch people out:

* **The request target includes the query string.** Sign
  `/v1/rounds?limit=5`, not `/v1/rounds`.
* **The body is the raw bytes you send**, byte-for-byte. If you re-serialize
  your JSON between signing and sending, the signature breaks.
* **The signature covers the body**, so you cannot sign once and replay with
  different parameters.

<Warning>
  **Base64-decode your secret before using it as the HMAC key.**

  The secret is 32 random bytes, handed to you as standard base64 — 44 characters,
  which may contain `+` and `/`. The HMAC key is those **32 raw bytes**, not the
  44-character string.

  Using the string directly produces a perfectly valid-looking signature and a
  permanent `401`. This is the single most common integration bug.
</Warning>

## Working examples

<CodeGroup>
  ```python Python theme={null}
  import base64, hashlib, hmac, json, time
  import requests

  BASE = "https://api.raeth.exchange"
  KEY_ID = "6"
  SECRET = base64.b64decode("<your base64 secret>")   # 32 raw bytes

  def request(method, target, body=b""):
      ts = str(int(time.time() * 1000))
      canonical = f"{ts}\n{method}\n{target}\n".encode() + body
      sig = base64.b64encode(
          hmac.new(SECRET, canonical, hashlib.sha256).digest()
      ).decode()
      return requests.request(
          method, BASE + target, data=body,
          headers={
              "DX-ACCESS-KEY": KEY_ID,
              "DX-ACCESS-TIMESTAMP": ts,
              "DX-ACCESS-SIGNATURE": sig,
              "Content-Type": "application/json",
          },
      )

  print(request("GET", "/v1/portfolio/balance").json())
  ```

  ```typescript TypeScript theme={null}
  import { createHmac } from "node:crypto";

  const BASE = "https://api.raeth.exchange";
  const KEY_ID = "6";
  const SECRET = Buffer.from("<your base64 secret>", "base64"); // 32 raw bytes

  export async function request(method: string, target: string, body = "") {
    const ts = Date.now().toString();
    const canonical = `${ts}\n${method}\n${target}\n${body}`;
    const sig = createHmac("sha256", SECRET).update(canonical).digest("base64");

    return fetch(BASE + target, {
      method,
      body: body || undefined,
      headers: {
        "DX-ACCESS-KEY": KEY_ID,
        "DX-ACCESS-TIMESTAMP": ts,
        "DX-ACCESS-SIGNATURE": sig,
        "Content-Type": "application/json",
      },
    });
  }

  console.log(await (await request("GET", "/v1/portfolio/balance")).json());
  ```

  ```bash cURL theme={null}
  TS=$(date +%s000)
  TARGET="/v1/portfolio/balance"
  SECRET_B64="<your base64 secret>"

  SIG=$(printf '%s\nGET\n%s\n' "$TS" "$TARGET" \
    | openssl dgst -sha256 -mac HMAC \
        -macopt "hexkey:$(echo "$SECRET_B64" | base64 -d | xxd -p -c 64)" \
        -binary | base64)

  curl -s "https://api.raeth.exchange$TARGET" \
    -H "DX-ACCESS-KEY: 6" \
    -H "DX-ACCESS-TIMESTAMP: $TS" \
    -H "DX-ACCESS-SIGNATURE: $SIG"
  ```
</CodeGroup>

## Clock skew

Your timestamp must be within **±5 seconds** of the server's clock. Outside that
window the request is rejected as a replay, with the same `401` as a bad
signature.

If you see intermittent `401`s that go away on retry, check your clock before
you check your signing code.

## Why a request failed

Every authentication failure returns the same response:

```json theme={null}
{ "error": { "code": "unauthorized", "origin": "gateway", "message": "unauthorized" } }
```

This is deliberate — an unknown key ID, a bad signature, and a stale timestamp
are indistinguishable, so probing tells an attacker nothing. Work through the
list in order:

1. Did you base64-decode the secret?
2. Does the signed target include the query string?
3. Is the signed body byte-identical to what you sent?
4. Is your clock within 5 seconds?
5. Is the key still live? A revoked key cannot be restored.

## Scopes

Keys carry scopes, and a bot key cannot withdraw funds. See
[API keys and scopes](/concepts/api-keys).
