CX Pay API

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:

HeaderValue
X-Key-IdYour API key ID (key_...)
X-TimestampISO-8601 UTC timestamp
X-NonceUnique string per request (UUID works)
X-Body-HashSHA-256 hex of the request body
X-SignatureBase64-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}
PartRules
METHODUppercase: POST, GET
PATHLeading /, no trailing /
SORTED_QUERYQuery params sorted by key (ASCII). Empty string if none.
TIMESTAMPSame as X-Timestamp header
NONCESame as X-Nonce header
BODY_HASHSHA-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 body

The 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,
    ];
}

On this page