Handle and verify callbacks
Verify raw bytes, deduplicate events, and acknowledge safely.
Verify before parsing
Read the raw request body before JSON middleware. Compute HMAC-SHA256 using the webhook's signing secret over timestamp + "." + rawBody, and compare the lowercase hex signature with a timing-safe comparison.
| Header | Meaning |
|---|---|
X-Webhook-Id | Durable event ID; unchanged across retries |
X-Webhook-Timestamp | Unix seconds, refreshed on delivery attempts |
X-Webhook-Signature | HMAC-SHA256 hex signature |
X-Webhook-Event | Event such as PAYMENT_SUCCESS |
Request authentication uses the same timestamp-dot-body structure but a different secret. Do not use the API secret for webhook verification. Header names are case-insensitive.
Receiver example
Preview, copy, or download verify-webhook.mjs. It checks the signature, envelope/header consistency, and environment. Its five-minute timestamp tolerance is a recommended receiver policy. Keep your clock synchronized.
Preview, copy, or download JavaScript verifier or Python helper. Node.js requires no framework. Express: npm install express. Python/FastAPI: python3 -m pip install fastapi uvicorn, then python3 -m uvicorn receiver:app --port 4000. Run Node.js/Express with node receiver.mjs. In Next.js, put the verifier in src/lib/ and use your existing app server. The database steps are comments so you can use your existing transaction code. These are integration templates: implement and await those steps before deployment; as written, the comments do not save anything.
Verify using only the webhook signing secret from the registered webhook, not your payment API secret. You do not need to pass TEST/LIVE to the helper. After successful verification, read event.environment (TEST or LIVE); this value comes from the signed payload, not from guessing the secret type. The helper checks that both environment fields agree.
You may still pass environment: 'LIVE' (Python: environment="LIVE") to restrict a receiver to LIVE events. A mismatch is rejected, never converted to another environment. Keep TEST events separate from real orders and match the verified environment to the saved payment before fulfillment. Switching payment API keys does not switch the webhook secret: configure the signing secret for the webhook delivering to this receiver. One secret does not verify both separately registered TEST and LIVE webhooks.
import { createServer } from 'node:http';
import { verifyWebhook } from './verify-webhook.mjs';
createServer(async (req, res) => {
if (req.method !== 'POST' || req.url !== '/callbacks/vantra') {
res.writeHead(404).end(); return;
}
const chunks = []; let size = 0;
for await (const chunk of req) {
size += chunk.length;
if (size > 1000000) { res.writeHead(413).end(); return; }
chunks.push(chunk);
}
const headers = new Headers();
for (const [name, value] of Object.entries(req.headers)) {
if (typeof value === 'string') headers.set(name, value);
}
let event;
try {
event = verifyWebhook(Buffer.concat(chunks), headers, {
secret: process.env.VANTRA_WEBHOOK_SECRET,
});
} catch { res.writeHead(400).end(); return; }
try {
// TODO: Add your database update here before returning 200.
// Match transactionId, orderId, environment, amount and currency to your saved payment.
// If event.data.status === 'SUCCESS', mark the matching transaction paid.
// Handle other statuses without marking them paid; do not overwrite an existing success.
// Save event.id and the update atomically; already processed events are a no-op.
// Await the database commit. Throw on failure so the gateway receives 503.
res.writeHead(200).end('OK');
} catch { res.writeHead(503).end(); }
}).listen(4000);Return HTTP 200 OK after the transaction update is committed. The gateway accepts any 2xx status, including 204; no special response body is required. This acknowledges callback receipt, not payment success: only mark the payment paid when the verified status is SUCCESS and the saved payment matches. Already processed events should also return 200. Return 400 for failed verification and 503 for temporary database failures so delivery can be retried.
Process the order
Before returning 200, look up your order using the saved payment mapping. Compare transactionId, orderId, environment, amount, and currency. Use decimal-safe comparisons: callback amounts are strings. Reject mismatches for manual review. Mark a successful order paid and schedule fulfillment in an atomic, idempotent operation. Do not fulfill twice even if separate deliveries or events arrive.
Do not rely on callback arrival order or a browser return. Keep the event log for reconciliation and do not overwrite an already fulfilled order with a delayed pending/failure state.