Skip to main content
SDKs are coming soon and will handle signing for you. Until then you build and sign every request yourself, and a small mistake makes the exchange reject it. This page serves two readers: someone implementing signing for the first time (work through the rules), and someone debugging a rejected request (start from the error you got). Two terms used below:
  • Payload — the bytes you sign for a write: [Header ‖ RequestId ‖ Body]. See Signed payloads.
  • Canonical message — for a few read endpoints, a short fixed byte string the endpoint documents. You sign that, not your JSON body. See Session signatures.

Start from the error you got

You gotWhat it almost always meansFix
400 request_timestamp_skewYour request_id timestamp is outside the time window.Generate the request id fresh
401The signature didn’t verify — wrong encoding, wrong bytes signed, wrong scheme, or wrong key length.Base64 · Sign the right bytes · Match your scheme
404 on a read you expected to workYour credential is scoped to a different subaccount.Stay within scope
415 unsupported_content_typeThe Content-Type header is missing or wrong.Set the content type
200 but it didn’t happenA 200 means processed, not accepted — read the body.A 200 can still be a rejection

The rules

Ordered by frequency.

1. Generate the request id fresh, every time

Your request_id (and X-REQUEST-ID on reads) must be a UUIDv7 generated at send time. UUIDv7 embeds a timestamp, and the exchange rejects a request whose timestamp is not current. Generate the id immediately before sending, keep your clock in sync (NTP), and do not reuse or pre-generate ids. A UUIDv4 (or any non-v7) has no usable timestamp and is rejected. A stale timestamp returns 400 request_timestamp_skew. The same id is also your idempotency key — resend with the same id after a timeout, and a duplicate returns duplicate_request_id instead of executing twice.

2. Encode every field as standard base64

Use standard base64 — the + / / alphabet with = padding. URL-safe base64 (- / _) does not decode and the request fails with 401. This applies to payload, signature, and public_key in the JSON envelope, and to X-PUBLIC-KEY / X-SIGNATURE on reads.

3. Sign the right bytes

This is the most common 401. You sign different things depending on the endpoint:
  • Writes (orders, cash, key/session management via the envelope) — sign the payload bytes.
  • Reads that use the header triple (list/create/delete API keys, device login) — sign the endpoint’s canonical message, not your JSON body.
Signing the JSON body where a canonical message is expected verifies against the wrong bytes and fails.

4. Match the scheme to your key

The three schemes are not interchangeable. Set signature_type in the header to your credential and produce exactly what that curve expects:
Your keysignature_typeWhat you sign
Session key (Ed25519)0the raw payload bytes
Master key (Secp256k1)1the EIP-712 digest of the payload (not the raw bytes)
Master key (Passkey)2a WebAuthn assertion from the authenticator
A mismatch fails with 401.

5. Stay within your credential’s scope

Scope is fail-closed: a credential pinned to one subaccount can only ever act on that subaccount — it can never reach a wider one. Asking for a subaccount or resource your credential isn’t scoped to returns 404 Not Found (not 403). So a surprise 404 on a read you expected to succeed usually means you’re using a credential pinned to a different subaccount.

6. Set the content type

Send a Content-Type the signed-write path accepts:
  • application/json — for the Base64SignedPayload envelope.
  • application/octet-stream — for the binary [payload ‖ public_key ‖ signature] frame.
Anything else (or none) returns 415 unsupported_content_type.

A 200 can still be a rejection

A 200 OK means the request was processed, not accepted — read the body to know which, per the error model. Error responses (the 400 / 401 / 404 / 415 above) use application/problem+json with a stable code — branch on code, not on the human-readable detail.

Field and length reference

Decode lengths must match the curve, or verification fails with 401.
Curvesignature_typePublic keySignature
Ed25519032 bytes64 bytes
Secp256k1133-byte compressed64 bytes (r ‖ s)
Passkey (P-256)233-byte compressedraw r ‖ s, plus the WebAuthn fields
The RequestId is always 16 raw bytes (the UUIDv7) and the Header is 8 bytes. Signed payloads has the full byte layout.