Webhooks
Cimplify pushes order, payment, and booking lifecycle events to your endpoint as JSON over HTTPS. Every delivery is HMAC-signed and replayed with exponential backoff if your server doesn't return a 2xx within 30 seconds.
Event types
| Event | Fires when |
|---|---|
order.created | A new order is placed. |
order.updated | Order line items, status, or totals change. |
order.completed | Order has been fulfilled and closed. |
order.cancelled | Order is cancelled by the customer or business. |
payment.completed | Provider settles a charge for an order. |
payment.failed | Provider rejects a charge. |
payment.refunded | A refund is processed against an order. |
booking.created | A scheduled service is booked. |
booking.cancelled | A booking is cancelled. |
booking.rescheduled | A booking moves to a new slot. |
inventory.low_stock | A SKU drops below its threshold. |
customer.created | A customer account is created. |
Payload
Every delivery is the same envelope. data is the resource at the moment of the event; subscribe to order.updated if you need a continuous stream.
{
"id": "evt_01HZ8XK4Q9F0J0Y7M2N3P4R5S6",
"type": "order.created",
"created_at": "2026-05-07T10:30:00Z",
"business_id": "bus_currents_electronics",
"environment": "live",
"data": {
"order_id": "ord_01HZ8…",
"status": "confirmed",
"currency": "GHS",
"total": "299.99"
}
}Signature verification
Each request carries an X-Cimplify-Signature header of the form sha256=…. The digest is HMAC-SHA-256 over the raw request body, keyed by the secret you received when registering the endpoint. Use a constant-time compare.
import express from "express";
import crypto from "node:crypto";
const app = express();
app.post(
"/webhooks/cimplify",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.header("x-cimplify-signature") ?? "";
const expected =
"sha256=" +
crypto
.createHmac("sha256", process.env.CIMPLIFY_WEBHOOK_SECRET!)
.update(req.body)
.digest("hex");
const sigBuf = Buffer.from(signature);
const expBuf = Buffer.from(expected);
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body.toString("utf8"));
enqueue(event); // process async
res.status(200).end();
},
);Retry policy
Anything other than a 2xx in under 30 seconds is treated as a failure. Cimplify retries with exponential backoff up to 24 hours, then marks the event undeliverable.
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | +1 minute |
| 3 | +5 minutes |
| 4 | +30 minutes |
| 5 | +2 hours |
| 6 | +8 hours, then dropped |
Idempotency on your receiver
Events may be delivered more than once. Treat event.id as the idempotency key for whatever side effect the event triggers (a database write, a downstream API call, an email). Storing the ID for ~7 days is enough to cover the retry window.
async function handle(event: { id: string; type: string; data: unknown }) {
const seen = await redis.set(`webhook:${event.id}`, "1", "EX", 7 * 24 * 3600, "NX");
if (seen !== "OK") return; // already processed
await processEvent(event);
}Local testing with the mock
The CLI mock can fan out events to any URL you supply. Combine with a tunnel (or a localhost receiver) to wire up your handler before going live.
cimplify mock --seed retail \
--webhook-url http://localhost:3000/webhooks/cimplify \
--webhook-secret whsec_local_devNext
-
Idempotency Replay-safe writes on the outbound side
-
API Keys Where the webhook secret fits
Errors
SDK methods never throw. They return a `Result` whose error branch is a typed `CimplifyError` with a stable `code`, a human-readable `message`, an optional `retryable` hint, and structured `context`.
Checkout lifecycle
Every Cimplify checkout (embedded iframe, hosted Pay session, or controlled React Element) passes through the same ordered set of states. Whether you observe them via `onStatusChange`, the `checkout_status` postMessage event, or the `on_status_change` callback on `processCheckout()`, the names and order are identical.