Webhooks Quick Start
Set up a webhook endpoint to receive and verify payment callbacks in minutes
3 min readUpdated Mar 26, 2026
Set up a minimal webhook handler: respond 200 OK immediately, verify the signature, deduplicate, then process.
#Prerequisites
- A publicly accessible HTTPS endpoint
- Your merchant secret key (from the Exirom dashboard)
#Minimal Webhook Handler
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhooks/payment', express.json(), async (req, res) => {
// 1. Respond 200 immediately — prevents Exirom from retrying
res.status(200).send('OK');
// 2. Verify signature
const received = req.headers['x-checksum'];
const { paymentMethod } = req.query;
const p = req.body;
const raw = paymentMethod === 'apm'
? `${p.accountId}|${p.amount}|${p.currency}|${p.transactionId}`
: `${p.mid}|${p.orderAmount}|${p.orderCurrency}|${p.transactionId}`;
const expected = crypto
.createHmac('sha256', process.env.MERCHANT_SECRET)
.update(raw)
.digest('base64');
if (expected !== received) {
console.error('Invalid signature — ignoring callback');
return;
}
// 3. Deduplicate
const key = `${p.transactionId}:${p.transactionStatus}`;
const exists = await db.webhookLog.findUnique({ where: { key } });
if (exists) return;
await db.webhookLog.create({ data: { key } });
// 4. Process
if (p.transactionStatus === 'SUCCEED') {
await fulfillOrder(p.transactionId);
} else if (p.transactionStatus === 'FAILED') {
await notifyCustomer(p.transactionId, p.declineCode);
}
});#What the Handler Does
| Step | Why |
|---|---|
Respond 200 first | Exirom retries if it doesn't receive 200 within 10 seconds |
Verify X-Checksum | Prevents forged callbacks from triggering fulfillment |
Deduplicate on transactionId + transactionStatus | Exirom delivers at-least-once — the same callback may arrive twice |
Check transactionStatus | SUCCEED → fulfill; FAILED → notify customer; PENDING → wait for webhook |
#Key Callback Fields
| Field | Description |
|---|---|
transactionId | Unique transaction ID — use as your deduplication key |
transactionStatus | SUCCEED, FAILED, PENDING, REFUNDED |
declineCode | Set when transactionStatus is FAILED — see Decline Codes Reference |
paymentMethod | Query param appended by Exirom: card or apm |
#Next Steps
- Webhook Overview — full payload schema and lifecycle diagram
- Callback Identification — routing Card vs APM vs HPP callbacks
- Webhook Best Practices — deduplication patterns, out-of-order handling, polling fallback
- Callback Retry Mechanism — retry schedule and backoff behavior
Was this helpful?