Appearance
Authentication
Every request is signed with HMAC-SHA256. The signature binds the timestamp, method, path and body together, so replaying or tampering with a request invalidates it.
Getting a 401? See Sign requests for how to debug it.
Headers
| Header | Type | Required | Purpose |
|---|---|---|---|
zay-api-key | string | required | Your merchant id (UUID) |
zay-timestamp | int | required | Unix timestamp in seconds |
zay-signature | string | required | Hex-encoded HMAC-SHA256 of the canonical payload |
Idempotency-Key | string | recommended | Quoted UUID per RFC 9651, e.g. "d7c59ab2-..." — see Idempotency |
Signature payload
Build the payload string by joining four parts with newlines, in this order:
timestamp— same value sent inzay-timestamp(unix seconds)method— HTTP verb in upper casepath— request path without the query string, e.g./merchants/v1/payoutsbody— raw request body as a string (empty string when there is no body)
payload = "{ts}\n{METHOD}\n{path}\n{body}"
signature = HMAC_SHA256(private_key, payload).hexdigest()
→ header = zay-signature: a3f1c9…zay-timestamp must be within ±5 minutes of server time, otherwise the request is rejected with 401.
Sign a request
cURL
bash
TS=$(date +%s)
METHOD="POST"
PATH_="/merchants/v1/payouts/report"
BODY='{"user_id":"a74a29ae-...","payout":{"amount":"100.0","currency_code":"USDC","transaction_hash":"0xabc123"}}'
PAYLOAD="$TS"$'\n'"$METHOD"$'\n'"$PATH_"$'\n'"$BODY"
SIG=$(printf "%s" "$PAYLOAD" \
| openssl dgst -sha256 -hmac "$ZAY_PRIVATE_KEY" \
| awk '{print $NF}')
echo "$SIG"Python
python
import hashlib
import hmac
import time
from urllib.parse import urlparse
def merchant_headers(method, url, private_key, body=""):
ts = str(int(time.time()))
path = urlparse(url).path
payload = f"{ts}\n{method.upper()}\n{path}\n{body}"
sig = hmac.new(
private_key.encode("utf-8"),
payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return {
"zay-timestamp": ts,
"zay-signature": sig,
}Node.js
javascript
import crypto from "node:crypto";
export function merchantHeaders(method, url, privateKey, body = "") {
const ts = Math.floor(Date.now() / 1000).toString();
const path = new URL(url).pathname;
const payload = `${ts}\n${method.toUpperCase()}\n${path}\n${body}`;
const sig = crypto
.createHmac("sha256", privateKey)
.update(payload)
.digest("hex");
return { "zay-timestamp": ts, "zay-signature": sig };
}Webhook deliveries from Zay to you use a different scheme — see Receive and verify webhooks.