The four required headers
Build the canonical request string
The signature is computed over a deterministic string both sides build identically:
Fields are joined with the ASCII colon
: (0x3A). No whitespace, no trailing newline, encoded as UTF-8.
The three most common mistakes
Including the query string in pathname
Including the query string in pathname
If your URL is
/wallets/main/paymentOrders?status=SUCCESS, the {pathname} field is /wallets/main/paymentOrders — the ?status=SUCCESS part is stripped. Sign the path only.Hashing a re-serialized body instead of raw bytes
Hashing a re-serialized body instead of raw bytes
Hash the exact bytes that will travel on the wire — not a re-serialized version. If you parse JSON into an object and re-stringify it before hashing, whitespace, key order, and escape sequences may differ from what your HTTP client eventually sends. Result:
SIGNATURE_INVALID.Reading the clock twice
Reading the clock twice
Read the current time once, store it in a variable, and use the same value in both the
X-Access-Timestamp header and the {timestamp} field of the canonical string. Reading the clock twice — even microseconds apart — gives you a different value in each place.Sign and encode
1
SHA-256 of the canonical string
Compute the SHA-256 digest of the UTF-8 bytes of the canonical request. This yields a 32-byte digest.
2
ECDSA sign
Sign the digest with your private key on
secp256k1.3
Low-S normalization (mandatory)
The signature must be in low-S form (i.e.,
s ≤ n/2). Most modern libraries do this by default; some (notably WebCrypto) do not. Enable the lowS / canonical / normalize_s flag. See Troubleshooting.4
Base64 encode
Standard Base64 (RFC 4648), not URL-safe. No line breaks. Both DER and P1363 (
r || s) signature encodings are accepted — whichever your library emits will work.5
Attach as X-Access-Signature
Send the Base64 result in the
X-Access-Signature header alongside the other three.Pseudocode
Worked example — GET with query, no body
Target request:
Canonical request:
?filter=status%3DACTIVE query stays on the URL line but is not included in the canonical request.
Worked example — POST with JSON body
Target request:
The canonical request follows the same format. If your serializer changes a single byte of the JSON between hashing and sending, you will get
SIGNATURE_INVALID — hash the bytes you are about to send, not a re-parsed copy.
Send the request
The final HTTP request:201 Created with the created payment order in the body.
Security checklist
- Never hardcode the private key. Load it from a secret manager.
- Never transmit the private key. The signature alone goes over the wire.
- Synchronize your clock. With a ±10-second window, NTP is mandatory.
- Generate a fresh
X-Access-Request-Idper attempt. Including retries. - Use HTTPS only. TLS encrypts the body; the signature only proves authenticity.
- Rotate credentials periodically. Every 6–12 months at minimum.
- Log
decisionIdandexcRecordIdon failure — they let support trace your request server-side. See Errors.
Next
Code examples
Working clients in Node.js, Python, Go, Java, PHP, and .NET.
Troubleshooting
Diagnose SIGNATURE_INVALID in 11 steps.
Errors
Branch on error.status, retry safely.