Webhooks
Verify payment events and handle delivery retries without applying an order twice.
Configure invoice events
Pass a public HTTPS webhook_url in the invoice creation request. A saved merchant setting is not automatically inherited by API-created invoices.
Use a public DNS hostname on standard HTTPS port 443, with no credentials or URL fragment. IP literals, private destinations, and redirects are not supported. The receiver must accept JSON POST requests.
Verify the signature
The signing message is timestamp + "." + rawRequestBody, using HMAC-SHA256 with the merchant's webhook signing secret. Do not parse and reserialize the JSON before verification.
Headers include X-CryptiumPay-Event-Id, X-CryptiumPay-Timestamp, X-CryptiumPay-Signature, and X-CryptiumPay-Signatures. Header names are case-insensitive. New integrations should verify the plural X-CryptiumPay-Signatures list, falling back to the singular header when the list is absent.
import { createHmac, timingSafeEqual } from 'node:crypto';
// Перевіряє точні байти, свіжість timestamp і будь-який чинний підпис під час ротації секрету.
export function verifyWebhook(
rawBody,
timestamp,
signatures,
secret,
now = Math.floor(Date.now() / 1000),
) {
if (typeof timestamp !== 'string' || !/^\d{10}$/.test(timestamp)) return false;
if (Math.abs(now - Number(timestamp)) > 300 || typeof signatures !== 'string') return false;
const expected = createHmac('sha256', secret)
.update(timestamp + '.')
.update(rawBody)
.digest();
for (const entry of signatures.split(',')) {
const match = /^sha256=([a-f0-9]{64})$/i.exec(entry.trim());
if (match && timingSafeEqual(expected, Buffer.from(match[1], 'hex'))) return true;
}
return false;
}Pass the raw bytes captured by your framework. After verification, parse the payload, verify the merchant/invoice/order against your records, and durably record the event before acknowledging it. The five-minute freshness window above is a recommended receiver policy; keep your server clock synchronized.
Event payload
{
"event_id": "44444444-4444-4444-8444-444444444444",
"sequence": "1024",
"type": "invoice.updated",
"created_at": "2026-10-24T14:10:00.000Z",
"merchant_id": "33333333-3333-4333-8333-333333333333",
"invoice_id": "22222222-2222-4222-8222-222222222222",
"order_id": "order-1001",
"status": "PAID",
"currency": "USD",
"amount": "19.95",
"paid_at": "2026-10-24T14:10:00.000Z",
"expires_at": "2026-10-24T15:00:00.000Z",
"crypto_payments": []
}The empty array keeps this example short. Payment entries contain network/token codes, address, status, expected and paid decimal/raw amounts, and transaction details. Amount fields are strings or null. sequence is a decimal string, not a JavaScript-safe number. Metadata, item snapshots, and success URLs are not included in invoice events.
An update is emitted when payment instructions are issued or settlement changes status or totals. Do not depend on receiving an initial event for every NEW invoice.
Duplicates and ordering
Delivery can repeat. Store processed event_id values and make fulfilment idempotent. An older event can arrive after a newer one. Compare sequence values with BigInt or an exact integer type before replacing invoice state. Keep that check scoped to the invoice whose state you update.
The payload, event ID, and sequence stay the same during a retry; the delivery timestamp and signatures are refreshed.
Secret rotation
API keys and webhook secrets are independent. During the 24-hour transition after webhook-secret rotation, the plural header contains sha256=current,sha256=previous. The singular compatibility header still uses the previous signature. After the window, only the current secret signs deliveries.
Use the plural list so a receiver configured with either secret can validate during the transition. Repeated rotation within that active window is rejected. Do not assume a legacy webhook secret continues to equal the API key.
Delivery and retries
Any 2xx response marks delivery successful. Network/DNS failures, 408, 429, and 5xx responses are retryable. Other non-2xx responses, including redirects and ordinary 4xx, end that delivery cycle.
An automatic cycle has up to 10 attempts. Initial retry delays are 10, 20, 40, 80, 160, 320, 640, 1280, and 2560 seconds. These are scheduling delays; worker load can make delivery later. HTTPS requests time out after 8 seconds; DNS lookup has a separate 3-second timeout.
The console delivery log shows attempts and errors. A manual retry of a failed event keeps its event ID and payload, so the receiver must still deduplicate. A test event has type merchant.webhook.test and test: true; acknowledge it without fulfilling an order.