Skip to content

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 ​

HeaderTypeRequiredPurpose
zay-api-keystringrequiredYour merchant id (UUID)
zay-timestampintrequiredUnix timestamp in seconds
zay-signaturestringrequiredHex-encoded HMAC-SHA256 of the canonical payload
Idempotency-KeystringrecommendedQuoted UUID per RFC 9651, e.g. "d7c59ab2-..." — see Idempotency

Signature payload ​

Build the payload string by joining four parts with newlines, in this order:

  1. timestamp — same value sent in zay-timestamp (unix seconds)
  2. method — HTTP verb in upper case
  3. path — request path without the query string, e.g. /merchants/v1/payouts
  4. body — 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.