Skip to main content
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.
Authentication proves who you are. It does not authorise the movement of any funds. Every order additionally carries your own EIP-712 signature, 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.

API-key (HMAC) authentication

Send three headers on every private request:

The canonical message

Concatenated with no separators. Each part is exact: 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.

The exact string signed, for a POST

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.

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.

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: Open your type below.
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 (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:
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:
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.

API overview

Base URLs, response format, and pagination.

Errors

The dot-coded error catalog.

Signed orders

The EIP-712 signature every order carries, separate from authentication.

Session keys

Browser-held keys that sign orders without a passkey prompt.