CreddotDocsCreddot Docs

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

TypeWhen
transaction.successA payment, payout or bill payment succeeded
transaction.failedIt failed
transaction.pending, transaction.processingIt started or is waiting on the provider
transaction.reversed, transaction.cancelledIt was reversed or cancelled
transaction.reconciliation_requiredThe provider's answer was unclear; Creddot is checking. Don't act yet.
payout.queuedA payout was accepted and is waiting to be sent
subscription.activated, subscription.renewedA subscriber paid their first or a renewal cycle
subscription.overdue, subscription.lapsed, subscription.cancelledA subscriber missed a payment, ran out of grace period, or cancelled
refund.requestedA 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:

HeaderValue
x-creddot-event-idThe event id; use it to ignore duplicates
x-creddot-timestampUnix time in seconds
x-creddot-signaturesha256= 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 2xx within 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-id to 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.

On this page