Every request to the External API must be signed with your Ed25519 private
key. The server verifies the signature against the public key stored for
your credential. Signing takes four steps: build the canonical payload, sign
it, encode the signature, and attach three headers to the request.Step 1 — Build the canonical payload#
The payload is a single pipe-delimited UTF-8 string:METHOD|PATH|VARIABLE|TIMESTAMP_MS
| Field | Value |
|---|
METHOD | HTTP method in uppercase: GET, POST, PUT, PATCH, DELETE |
PATH | URL path only, without the query string: /api/v1/organizations/acme/positions |
VARIABLE | For GET and DELETE — the raw query string (everything after ?). For all other methods — the raw request body, byte-for-byte as it will be sent. Empty string if there is no query or body. |
TIMESTAMP_MS | Current Unix time in milliseconds. The exact same value must be sent in the X-Timestamp-Ms header. |
# GET with query parameters
GET|/api/v1/organizations/acme/positions|status=open&page_size=50|1716643200000
# GET with no query parameters (VARIABLE is empty)
GET|/api/v1/organizations/acme/positions||1716643200000
# POST with a JSON body
POST|/api/v1/organizations/acme/orders|{"asset":"BTC","quantity":"1.5"}|1716643200000
Step 2 — Sign with Ed25519#
Sign the UTF-8 bytes of the canonical payload with your private key using
standard Ed25519 (RFC 8032). Do not hash the payload yourself —
Ed25519 hashes internally.Private key format. The key returned at credential creation is a
base64url-encoded 64-byte Ed25519 private key (32-byte seed followed by
the 32-byte public key). If your crypto library expects only the 32-byte
seed, use the first 32 bytes: privateKey[:32].
Step 3 — Encode the signature#
Base64url-encode the 64-byte signature without padding (no trailing =
characters). The result is always 86 characters long.Step 4 — Attach the headers and send#
| Header | Value |
|---|
X-API-Key | Your Ed25519 public key, base64url without padding (43 chars) |
X-Timestamp-Ms | The timestamp from Step 1, as a decimal integer |
X-Signature | The encoded signature from Step 3 |
Ready-to-use client implementations in Go, Python, and TypeScript are
available in the Code Examples section.Replay protection#
The server treats X-Timestamp-Ms as a monotonic nonce: each request must
carry a value strictly greater than the last accepted one for that
credential. There is no timestamp window — using the current system time in
milliseconds is sufficient for a single-process client. If multiple
processes share one credential, they must coordinate nonce values; the
simpler option is to issue a separate credential per process.Common pitfalls#
Signature mismatch (invalid api credential signature). Almost always
a payload-construction issue. Check that:PATH does not include the query string, and VARIABLE does not
include the leading ?.
The body in the payload is byte-identical to the body actually sent —
serialize the JSON once and reuse the same string for both.
The timestamp in the payload and in X-Timestamp-Ms are the same value.
You use base64url encoding (with - and _), not standard base64,
and you strip the = padding.
api credential request timestamp is too old. The timestamp was not
greater than the previous request's — typically caused by concurrent
requests from one credential or by clock rollback. See Replay protection
above.
API key headers ignored. If the request also carries an
Authorization: Bearer <token> header, JWT authentication takes
precedence and the signature headers are not evaluated. Remove the
Authorization header for API key requests.
Modified at 2026-06-16 11:15:56