Webhooks
Get told when payments, payouts and subscriptions change, and verify every event.
Creddot sends an HTTPS POST to your webhook URL when something changes. Set the URL and get its signing secret in the dashboard under Settings, API Keys and Webhooks. Test mode and live mode have separate URLs and secrets.
Event format
{
"id": "cmv9x1...",
"type": "transaction.success",
"createdAt": "2026-11-02T10:15:00.000Z",
"data": {
"id": "cmv9w8...",
"reference": "pl_cmv8l2..._6dc9...",
"status": "SUCCESS",
"type": "COLLECTION",
"amountMinor": "250000",
"currency": "NGN",
"environment": "SANDBOX",
"checkoutSessionId": "cmv8l2...",
"merchantReference": "ORDER-10042"
}
}checkoutSessionId and merchantReference appear on payments made through a checkout session.
Event types
| Type | When |
|---|---|
transaction.success | A payment, payout or bill payment succeeded |
transaction.failed | It failed |
transaction.pending, transaction.processing | It started or is waiting on the provider |
transaction.reversed, transaction.cancelled | It was reversed or cancelled |
transaction.reconciliation_required | The provider's answer was unclear; Creddot is checking. Don't act yet. |
payout.queued | A payout was accepted and is waiting to be sent |
subscription.activated, subscription.renewed | A subscriber paid their first or a renewal cycle |
subscription.overdue, subscription.lapsed, subscription.cancelled | A subscriber missed a payment, ran out of grace period, or cancelled |
refund.requested | A refund was requested |
Always switch on type and ignore types you don't handle; new types may be added.
Verify the signature
Every request has these headers:
| Header | Value |
|---|---|
x-creddot-event-id | The event id; use it to ignore duplicates |
x-creddot-timestamp | Unix time in seconds |
x-creddot-signature | sha256= followed by the hex HMAC-SHA256 of {timestamp}.{raw body}, keyed with your webhook secret |
Verify against the raw request body, before parsing it, and reject old timestamps.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyCreddotWebhook(rawBody, headers, secret) {
const timestamp = headers['x-creddot-timestamp'];
const signature = headers['x-creddot-signature'] ?? '';
if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = 'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}
// Express: keep the raw body for this route
app.post('/webhooks/creddot', express.raw({ type: 'application/json' }), async (req, res) => {
const raw = req.body.toString('utf8');
if (!verifyCreddotWebhook(raw, req.headers, process.env.CREDDOT_WEBHOOK_SECRET)) return res.sendStatus(400);
const event = JSON.parse(raw);
if (await alreadyHandled(event.id)) return res.sendStatus(200);
if (event.type === 'transaction.success' && event.data.merchantReference) {
await markOrderPaid(event.data.merchantReference, event.data.id);
}
res.sendStatus(200);
});Delivery and retries
- Respond with any
2xxwithin 10 seconds. Do slow work after responding. - Anything else is retried, up to 5 attempts, waiting 1, 2, 4 and 8 minutes between them (at most an hour).
- The same event can arrive more than once. Use
x-creddot-event-idto process it once. - Events can arrive out of order. When the order matters, read the current state with
GET /transactions/{id}. - You can see each transaction's delivery history and resend events from the transaction in the dashboard.