Webhooks
Receive signed OfferKit events, verify payloads, make handlers idempotent, recover failed deliveries, and test retries and manual replay.
Webhooks notify your server after OfferKit records an event. Use them for downstream side effects such as messages, analytics, fulfillment, and reconciliation. Do not use a webhook as the only record of a checkout result; store the direct API outcome with the order.
Create an endpoint
Open Webhooks in the dashboard and create an endpoint, or call
POST /api/v1/webhooks. Select * for every event or list the event types the handler
understands.
The plaintext signing secret is shown once. Store it in your server’s secret manager.
Receive the raw request
Each delivery is a JSON POST:
{
"id": "event-uuid",
"type": "voucher.redeemed",
"payload": {
"redemptionId": "redemption-uuid",
"voucherCode": "WELCOME10",
"amount": 1000
},
"createdAt": "2026-05-08T22:25:00Z"
}Headers include:
X-Offerkit-Event-TypeX-Offerkit-Event-IdX-Offerkit-Signature: t=<unix>,v1=<hex>
Preserve the unparsed body bytes for signature verification.
Verify the signature
The signature is HMAC-SHA256(secret, "<t>.<rawBody>"). The TypeScript SDK provides
constant-time verification and rejects old timestamps by default:
import { verifyWebhook } from "@offerkit/sdk";
export async function handleOfferKitWebhook(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get("x-offerkit-signature") ?? "";
if (!verifyWebhook(rawBody, signature, process.env.OFFERKIT_WEBHOOK_SECRET!)) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(rawBody);
await processOnce(event.id, event);
return new Response(null, { status: 204 });
}Never verify a re-serialized JSON object. Whitespace or key-order changes produce a different signature.
Make processing idempotent
Deliveries are at least once. Store the event ID in the same transaction as the side effect or enqueue it into an idempotent worker. If the event was already processed, return success without repeating the action.
Respond quickly after durable acceptance. Do slow email, analytics, or fulfillment work asynchronously rather than holding the HTTP request open.
Retries and replay
Non-2xx responses and transport errors retry on this schedule:
1 minute → 5 minutes → 30 minutes → 2 hours → 12 hours → 24 hours → deadThe webhook detail page shows response status, body, error, attempt count, and next retry. Replay creates a new attempt for the same event while preserving delivery history. Your event-ID deduplication must therefore work for manual replays too.
Current event catalog
customer.createdvoucher.redeemedvoucher.redeemed.rolled_backvoucher.stack.redeemed
Subscribe to * only if your handler safely ignores unknown future event types. Prefer
an explicit default branch that records and acknowledges an unsupported type.
Test before launch
- a valid signature;
- a modified body;
- an old timestamp;
- the same event delivered twice;
- a handler timeout followed by retry;
- an unknown event type;
- replay from the dashboard;
- secret rotation in your deployment process.
Checkout integration
Integrate OfferKit with checkout by previewing incentives as carts change, committing redemptions safely, and handling retries without double-spend.
Test an integration
Test successful, rejected, concurrent, and retried promotion flows so validation, redemption, balances, rewards, and webhooks behave safely.