Skip to main content
Every write endpoint on the ZLL Trading API takes a signed request: order entry, cash movement, account and portfolio operations, master keys, and sessions. You build a binary payload, sign the decoded bytes with your credential’s private key, and send the signature alongside. The exchange verifies the signature before it acts. This is the byte-level signing contract. The header-triple format used by session, key, and device-management reads lives in Session signatures.

The envelope

Send a Base64SignedPayload JSON body. It has three standard-base64 fields.
{
  "payload":    "<base64 of [Header || RequestId || Body]>",
  "signature":  "<base64 of the signature over the decoded payload bytes>",
  "public_key": "<base64 of your signing credential's public key>"
}
payload
string
required
Base64 of the packed binary payload bytes ([Header || RequestId || Body]).
signature
string
required
Base64 of the signature. Sign the decoded payload bytes, not the base64 text.
public_key
string
required
Base64 of the signing credential’s public key.
The exchange decodes payload, then verifies signature against public_key over those exact bytes. signature and public_key sit beside payload, never inside it.
Use standard base64 (+ / / alphabet, = padding) for every field. URL-safe base64 (- / _) is rejected. This is the most common signing mistake — the signing checklist covers the rest.

The decoded payload layout

payload decodes to a packed, little-endian byte sequence (C struct layout):
[ Header (8 bytes) ][ RequestId (16 bytes) ][ Body (variable) ]

Header (8 bytes)

OffsetSizeFieldValue
01version1
11signature_type0 = Ed25519, 1 = Secp256k1, 2 = Passkey
22request_type16-bit little-endian code selecting the Body
44paddingzero
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.
The 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 a PasskeySignedPayload instead of Base64SignedPayload. It carries the same base64 payload plus the WebAuthn fields.
{
  "payload":            "<base64 of [Header || RequestId || Body]>",
  "signature":          "<base64 raw r || s>",
  "credential_id":      "<base64 WebAuthn credential id>",
  "authenticator_data": "<base64 authenticator data>",
  "client_data_json":   "<base64 client data JSON>",
  "public_key":         "<base64 compressed P-256 key, optional>"
}
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 an application/octet-stream body. Concatenate the same parts, no base64:
[ payload || public_key || signature ]
This carries the identical payload, public_key, and signature as the JSON envelope.

Procedure

1

Build the Body

Pack the endpoint’s body fields in declared order, little-endian, padded to an 8-byte boundary.
2

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.
3

Concatenate the payload

payload = Header || RequestId || Body.
4

Sign

Sign the decoded payload bytes with your credential’s private key, using the input that curve expects (see Per-curve signing).
5

Encode and send

Standard-base64 payload, signature, and public_key into the envelope, set Content-Type: application/json, then POST it.

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 — version 1, Ed25519 (0), request_type 0, 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.
Sign 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 returns 200 OK with a RequestAck body:
status
string
required
Outcome status, snake_case. request_completed means accepted. Any other value is a rejection — read it.
processed_at_ns
integer
required
Exchange processing timestamp, nanoseconds since the Unix epoch.
Some account, master-key, and session endpoints return their own response object with a 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.