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

# Authentication

> How to authenticate a private request: mint an API key and HMAC-sign every call.

Private requests authenticate with an **API key**: mint a key and secret, then
HMAC-sign every request.

Calibri validates the signature and authenticates you as the member the key
belongs to for the rest of the request.

Public endpoints need no authentication.

<Warning>
  Authentication proves **who you are**. It does not authorise the movement of any funds. Every order additionally carries your own [EIP-712 signature](/non-custodial/signed-orders), and every transfer out of your Safe is a transaction you sign. A compromised API key can read your account — it cannot spend from your Safe.
</Warning>

## API-key (HMAC) authentication

Send three headers on every private request:

| Header | Value |
| - | - |
| `X-Auth-Apikey` | Your API key |
| `X-Auth-Nonce` | Current Unix time in **milliseconds**, as a decimal string |
| `X-Auth-Signature` | HMAC-SHA256 of the canonical message below, keyed with your API secret, hex-encoded |

### The canonical message

```
nonce + apiKey + METHOD + path + body
```

Concatenated with **no separators**. Each part is exact:

| Part | Rule |
| - | - |
| `nonce` | The same value you send in `X-Auth-Nonce` |
| `apiKey` | The same value you send in `X-Auth-Apikey` |
| `METHOD` | HTTP method, **upper-cased** |
| `path` | The request target **exactly as sent** — leading `/`, full `/api/v2/…` prefix, and the **query string** if there is one. Not normalised, not stripped |
| `body` | The raw request body, byte-for-byte. The **empty string** for `GET`/`DELETE` and any bodyless request |

Because the method, path, and body are covered, one signature authorises exactly
**one** request. It cannot be replayed against a different path, a different
`limit=`, or different body contents.

<CodeGroup>
  ```javascript sign.js theme={null}
  const crypto = require("crypto");
  const apiKey = "123456...";
  const secret = "ABCDEF...";

  // path = the request target EXACTLY as you send it, query string included.
  // body = the raw request body bytes; "" for GET/DELETE.
  module.exports = (method, path, body = "") => {
    const nonce = String(Date.now());
    const message = nonce + apiKey + method.toUpperCase() + path + body;
    const signature = crypto
      .createHmac("sha256", secret)
      .update(message)
      .digest("hex");

    return {
      "Content-Type": "application/json;charset=utf-8",
      "X-Auth-Apikey": apiKey,
      "X-Auth-Nonce": nonce,
      "X-Auth-Signature": signature,
    };
  };
  ```

  ```python sign.py theme={null}
  import hashlib, hmac, time

  API_KEY = "123456..."
  SECRET = "ABCDEF..."

  def sign(method: str, path: str, body: str = "") -> dict:
      nonce = str(int(time.time() * 1000))
      message = nonce + API_KEY + method.upper() + path + body
      signature = hmac.new(
          SECRET.encode(), message.encode(), hashlib.sha256
      ).hexdigest()
      return {
          "Content-Type": "application/json;charset=utf-8",
          "X-Auth-Apikey": API_KEY,
          "X-Auth-Nonce": nonce,
          "X-Auth-Signature": signature,
      }
  ```

  ```go sign.go theme={null}
  package main

  import (
  	"crypto/hmac"
  	"crypto/sha256"
  	"encoding/hex"
  	"fmt"
  	"strings"
  	"time"
  )

  const apiKey, secret = "123456...", "ABCDEF..."

  // path is the request target exactly as sent, query string included.
  // body is "" for GET/DELETE.
  func sign(method, path, body string) map[string]string {
  	nonce := fmt.Sprintf("%d", time.Now().UnixMilli())
  	message := nonce + apiKey + strings.ToUpper(method) + path + body

  	mac := hmac.New(sha256.New, []byte(secret))
  	mac.Write([]byte(message))

  	return map[string]string{
  		"Content-Type":     "application/json;charset=utf-8",
  		"X-Auth-Apikey":    apiKey,
  		"X-Auth-Nonce":     nonce,
  		"X-Auth-Signature": hex.EncodeToString(mac.Sum(nil)),
  	}
  }
  ```
</CodeGroup>

### The exact string signed, for a POST

```
1700000000000abc123POST/api/v2/atlas/account/orders{"market":"1","side":"yes"}
└─ nonce ────┘└ kid ┘└ M ┘└─ path (query included) ─────────────┘└─ raw body ───────────┘
```

<Note>
  Sign the body you actually send. If your HTTP client re-serialises JSON — reordering keys or changing whitespace — after you compute the signature, the signature will not match. Build the body string once, sign that string, and transmit it verbatim.
</Note>

### Clock skew

`X-Auth-Nonce` is checked against server time. Read
`GET /api/v2/atlas/public/timestamp` (Unix **milliseconds**) to measure your
offset before signing.

## WebSocket

The private WebSocket upgrade is signed exactly like a REST call, so the signed
`path` must include the **full `?stream=…` query string** you connect with.
Signing the bare path will fail.

```javascript theme={null}
const sign = require("./sign");

const path =
  "/api/v2/websocket/private?stream=12.ob-inc&stream=order&stream=contract&stream=balance";

const headers = sign("GET", path); // GET, path-with-query, empty body
```

## Authentication is not authorisation to trade

Signing the request proves who you are. It does not authorise an order — every
non-custodial order carries **your own EIP-712 signature** as well, and whether
your client can produce one depends entirely on what owns your Safe. Check
`signature_type` on `GET /api/v2/atlas/account/wallet` before you build.

The number tells you what owns your Safe, which is what decides how your client
signs — and whether it can sign at all:

| `signature_type` | What owns your Safe | Can your own code sign orders? |
| - | - | - |
| **`3`** | A passkey | Only after you add a [second signing key](/non-custodial/signing-keys) — a passkey cannot sign outside its browser |
| **`2`** | An address you hold the private key for (MetaMask, Rainbow, WalletConnect) | Yes — your key signs the order directly |
| **Custodial** | Calibri holds your funds; there is no Safe | No signature is involved — orders are ordinary API calls |

Open your type below.

<Tabs>
  <Tab title="3 — passkey wallet">
    **Reads work as normal.** For writes it depends on whether you have added a
    second signing key.

    A passkey signs only in the browser it was registered in, and session keys are
    generated non-extractable, so neither can reach a server. Without a second key
    your client can read balances, positions, orders, contracts and market data,
    and nothing else.

    **With** a second [signing key](/non-custodial/signing-keys) (Settings →
    Wallet → Signing keys — adding one requires two-factor authentication) your
    client signs as an owner of your Safe: orders, withdrawals and redemptions
    all work.
    The order stays `signature_type: 3` with `maker == signer == your Safe` — what
    changes is the signature, which becomes a standard Safe owner signature:

    ```
    data        = abi.encode(bytes32 orderHash)
    messageHash = EIP-712 SafeMessage(bytes message), domain = (chainId, your Safe)
    signature   = your key's signature over messageHash   // 65 bytes, v ∈ {27,28}
    ```

    Sign the SafeMessage, not the order hash — the Safe hashes it again itself, so
    signing the order hash directly always fails validation. The symptom is a
    `422` reading *signed\_order signature was not accepted by the maker wallet*,
    on a signature that recovers to your key perfectly when you check it
    yourself — the Safe is recovering against a different digest.

    Computed OFFLINE. Your Safe exposes `getMessageHash(bytes)` and will return
    the same value, but that needs an RPC connection to the chain, which an API
    client generally has no reason to hold:

    ```js theme={null}
    import { ethers } from "ethers";

    // chainId and your Safe both come from GET /api/v2/atlas/account/wallet
    // (`chain_id` and `proxy_address`). No chain access is required.
    const data = ethers.AbiCoder.defaultAbiCoder().encode(["bytes32"], [orderHash]);

    const safeMessageHash = ethers.TypedDataEncoder.hash(
      { chainId, verifyingContract: safeAddress },
      { SafeMessage: [{ name: "message", type: "bytes" }] },
      { message: data }
    );

    const signature = wallet.signingKey.sign(safeMessageHash).serialized;
    ```

    The domain is `chainId` and `verifyingContract` only — no `name`, no
    `version`. That is Safe 1.3.0's domain, and getting it wrong fails exactly
    the same way as signing the raw order hash, so verify once against
    `getMessageHash` if you can reach a node.
  </Tab>

  <Tab title="2 — your own wallet">
    **Full access.** Your Safe is owned by an address whose private key you hold,
    so your client signs orders itself — an ordinary EIP-712 secp256k1 signature
    over the exchange `Order` struct.

    `maker` is your Safe and `signer` is your wallet address. The same key signs
    `execTransaction`, so withdrawals and redemptions are scriptable too. See
    [Signed orders](/non-custodial/signed-orders).

    A browser wallet like MetaMask will not hand its key to a server — use an
    account you control directly.
  </Tab>

  <Tab title="Custodial">
    **Nothing extra to sign.** Orders placed from your Calibri balance carry no
    order signature at all, and withdrawals go through the normal withdrawal
    endpoints. The API key is everything your client needs.
  </Tab>
</Tabs>

## Related

<CardGroup cols={2}>
  <Card title="API overview" href="/api-reference/introduction">
    Base URLs, response format, and pagination.
  </Card>

  <Card title="Errors" href="/api-reference/errors">
    The dot-coded error catalog.
  </Card>

  <Card title="Signed orders" href="/non-custodial/signed-orders">
    The EIP-712 signature every order carries, separate from authentication.
  </Card>

  <Card title="Session keys" href="/non-custodial/session-keys">
    Browser-held keys that sign orders without a passkey prompt.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.