The envelope
Send aBase64SignedPayload JSON body. It has three standard-base64 fields.
Base64 of the packed binary payload bytes (
[Header || RequestId || Body]).Base64 of the signature. Sign the decoded
payload bytes, not the base64 text.Base64 of the signing credential’s public key.
payload, then verifies signature against public_key over
those exact bytes. signature and public_key sit beside payload, never inside it.
The decoded payload layout
payload decodes to a packed, little-endian byte sequence (C struct layout):
Header (8 bytes)
| Offset | Size | Field | Value |
|---|---|---|---|
| 0 | 1 | version | 1 |
| 1 | 1 | signature_type | 0 = Ed25519, 1 = Secp256k1, 2 = Passkey |
| 2 | 2 | request_type | 16-bit little-endian code selecting the Body |
| 4 | 4 | padding | zero |
request_type picks the Body shape for the endpoint. Each endpoint documents its
code (for example place_limit_order = 0, create_session = 13). Multi-variant
endpoints (order update, transfer, account and master-key creation) select the variant
by setting request_type.
RequestId (16 bytes)
A UUIDv7, as raw 16 bytes. Two rules:- It must be a UUIDv7. The exchange reads the timestamp embedded in the id. A non-v7 id has no parseable timestamp and is rejected.
- Its timestamp must be current. The exchange rejects a request whose timestamp is too old or too far in the future. Generate the UUIDv7 at send time and keep your clock in sync.
RequestId is also the idempotency and dedup key. A replay of the same id returns
duplicate_request_id instead of executing twice — reuse it when retrying a write after
a timeout.
Body
The endpoint’s body fields, packed in declared order (C struct layout), little-endian, with trailing padding zeroed to an 8-byte boundary. Integer fields documented as raw integer units are the on-wire integers — no decimal scaling. Enum fields are their integer code.Per-curve signing
payload is identical for every credential type. Only how you sign it and what you put
in signature and public_key differ. Set signature_type in the Header to match the
credential you sign with. The three schemes are not interchangeable: a mismatch fails
verification with 401.
Ed25519
signature_type = 0. Sign the raw payload bytes.
Public key: 32 bytes. Signature: 64 bytes.
Used by session keys: orders, cash, sessions.Secp256k1
signature_type = 1. Sign the EIP-712 digest of the payload, not the raw bytes.
Public key: 33-byte compressed. Signature: 64 bytes (r || s).
Used by master keys: account and key management.Passkey
signature_type = 2. Carry a WebAuthn assertion.
Public key: 33-byte compressed P-256. Signature: raw r || s, plus WebAuthn fields.
Used by master keys (passkey flavour).Passkey envelope
A passkey master key sends aPasskeySignedPayload instead of Base64SignedPayload. It
carries the same base64 payload plus the WebAuthn fields.
public_key is optional for passkeys; when absent, the exchange resolves it from
credential_id. Master keys covers which key signs what.
Binary frame alternative
Write endpoints also accept anapplication/octet-stream body. Concatenate the same
parts, no base64:
payload, public_key, and signature as the JSON envelope.
Procedure
Build the Body
Pack the endpoint’s body fields in declared order, little-endian, padded to an
8-byte boundary.
Build the Header and RequestId
Set
version = 1, signature_type for your curve, and the endpoint’s request_type.
Generate a fresh UUIDv7 for RequestId.Sign
Sign the decoded
payload bytes with your credential’s private key, using the input
that curve expects (see Per-curve signing).Worked example: place a limit order
POST /api/v1/trading/order/place/limit, body PlaceLimitOrder,
request_type = place_limit_order (0), signed by an Ed25519 session key.
- Header =
01 00 0000 00000000— version1, Ed25519 (0), request_type0, 4 padding bytes. - RequestId = 16 bytes of a fresh UUIDv7.
- Body (
PlaceLimitOrder), packed in order:portfolio_id:account_id(64-bit) ‖subaccount_index(32-bit) ‖portfolio_index(32-bit).price: 64-bit unsigned, raw integer units (no decimals).quantity: 64-bit signed, raw integer units. The sign is the side — positive = buy/long, negative = sell/short.flags(OrderFlags):expiry(64-bit unsigned — TIF sentinel, see below),post_only,reduce_only,stp.asset: 16-bit id, then 2 padding bytes.
Header || RequestId || Body, then submit { payload, signature, public_key }
base64-encoded.
The expiry field is the time-in-force sentinel: 0 = immediate-or-cancel (IOC),
1 = fill-or-kill (FOK), the maximum 64-bit value = good-till-cancelled (GTC). Any
other value is good-till-time — a nanosecond Unix timestamp.
Read the body
A signed write that passes signature and skew checks returns200 OK with a
RequestAck body:
Outcome status, snake_case.
request_completed means accepted. Any other value is a
rejection — read it.Exchange processing timestamp, nanoseconds since the Unix epoch.
success boolean instead of a RequestAck. Either way, a 200 does not mean your write
was accepted — inspect the body on every response. See the error model
for the rule and the error codes for the full status list.