Skip to main content

Signing Requests

Every cashout call — balances, create, status — must be signed with the secret you generated when you enabled the API. All requests also carry your X-Api-Key and must come from an allowlisted IP.

The two signing headers

HeaderTypeRequiredDescriptionExample
X-TimestampstringYesCurrent Unix time in seconds. Rejected (signature_stale) if more than 5 minutes from the server clock.1783369120
X-Signaturestring (hex)YesHMAC_SHA256(secret, X-Timestamp + "." + rawBody), hex-encoded. For GET requests the body is the empty string.a7b9…

The signing string is the timestamp, a literal ., then the exact raw request body you send. Compute the signature over the bytes you actually transmit — any change to the body (re-serialization, key reordering, whitespace) invalidates it.

Sign the exact raw bytes

Build the signature from the same byte string you put on the wire — not a re-encoded copy. Serialize the body once, sign that string, and send that string.

Node.js

import crypto from 'node:crypto';

function sign(secret, body) {
const ts = Math.floor(Date.now() / 1000).toString();
const sig = crypto.createHmac('sha256', secret).update(`${ts}.${body}`).digest('hex');
return { ts, sig };
}

Shell (openssl)

TS=$(date +%s); BODY="" # empty for GET; the exact JSON string for POST
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | awk '{print $2}')

Next