Authentication
HMAC-SHA256 request signing — every request, every time.
Every API request is signed with HMAC-SHA256. This proves you hold the secret and that the request hasn't been tampered with.
Required Headers
Every request needs these five headers:
| Header | Value |
|---|---|
X-Key-Id | Your API key ID (key_...) |
X-Timestamp | ISO-8601 UTC timestamp |
X-Nonce | Unique string per request (UUID works) |
X-Body-Hash | SHA-256 hex of the request body |
X-Signature | Base64-encoded HMAC-SHA256 signature |
How to Sign
1. Build the canonical string
Six values, joined by newlines:
{METHOD}\n{PATH}\n{SORTED_QUERY}\n{TIMESTAMP}\n{NONCE}\n{BODY_HASH}| Part | Rules |
|---|---|
| METHOD | Uppercase: POST, GET |
| PATH | Leading /, no trailing / |
| SORTED_QUERY | Query params sorted by key (ASCII). Empty string if none. |
| TIMESTAMP | Same as X-Timestamp header |
| NONCE | Same as X-Nonce header |
| BODY_HASH | SHA-256 hex of body. No body = SHA-256 of empty string |
2. Sign it
signature = base64( hmac_sha256( base64_decode(secret), canonical_string ) )Your secret is Base64-encoded. Decode it first, then use it as the HMAC key.
3. Send it
Include all five headers. Done.
Timestamps must be within 5 minutes of the server time. Each nonce can only be used once. This prevents replay attacks.
Example
POST /checkout-sessions with body {"mode":"payment","amount":5000,"currency":"USD"}:
POST
/checkout-sessions
2026-04-07T18:30:00.000Z
550e8400-e29b-41d4-a716-446655440000
a3f2b8c1... ← SHA-256 hex of bodyThe empty third line = no query parameters.
Code Examples
Node.js
import crypto from "crypto";
function signRequest({ method, path, body, keyId, secret }) {
const timestamp = new Date().toISOString();
const nonce = crypto.randomUUID();
const bodyStr = body ? JSON.stringify(body) : "";
const bodyHash = crypto.createHash("sha256").update(bodyStr).digest("hex");
const canonical = [method, path, "", timestamp, nonce, bodyHash].join("\n");
const sig = crypto
.createHmac("sha256", Buffer.from(secret, "base64"))
.update(canonical)
.digest("base64");
return {
"Content-Type": "application/json",
"X-Key-Id": keyId,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Body-Hash": bodyHash,
"X-Signature": sig,
};
}Python
import hashlib, hmac, base64, json
from datetime import datetime, timezone
from uuid import uuid4
def sign_request(method, path, body, key_id, secret):
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
nonce = str(uuid4())
body_str = json.dumps(body) if body else ""
body_hash = hashlib.sha256(body_str.encode()).hexdigest()
canonical = f"{method}\n{path}\n\n{timestamp}\n{nonce}\n{body_hash}"
sig = base64.b64encode(
hmac.new(base64.b64decode(secret), canonical.encode(), hashlib.sha256).digest()
).decode()
return {
"Content-Type": "application/json",
"X-Key-Id": key_id,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Body-Hash": body_hash,
"X-Signature": sig,
}PHP
function signRequest(string $method, string $path, ?array $body, string $keyId, string $secret): array {
$timestamp = gmdate('Y-m-d\TH:i:s.000\Z');
$nonce = bin2hex(random_bytes(16));
$bodyStr = $body ? json_encode($body) : '';
$bodyHash = hash('sha256', $bodyStr);
$canonical = implode("\n", [$method, $path, '', $timestamp, $nonce, $bodyHash]);
$sig = base64_encode(hash_hmac('sha256', $canonical, base64_decode($secret), true));
return [
'Content-Type' => 'application/json',
'X-Key-Id' => $keyId,
'X-Timestamp' => $timestamp,
'X-Nonce' => $nonce,
'X-Body-Hash' => $bodyHash,
'X-Signature' => $sig,
];
}