Webhooks allow your application to receive real-time notifications when events happen in your Credibill account.
Overview
Credibill uses webhooks to notify your application when an event happens in your account. Webhooks are particularly useful for asynchronous events like when a Payment Service Provider(Flutterwave, PesaPal, DPO, pawaPay) confirms a payment, a customer disputes a charge, or a recurring payment succeeds.
Configuring Webhooks
- Go to the Webhooks section in your Dashboard. This is found under Settings → Webhooks.
- Click Add Webhook.
- Enter the URL of your server endpoint (e.g.,
https://api.mycoolapp.com/webhooks/credibill).
- Select the events you want to listen to.
Event Structure
Every webhook event payload has a consistent structure.
{
"id": "evt_123456",
"object": "event",
"type": "invoice.paid",
"created": 1678900000,
"data": {
"object": {
"id": "in_123456",
"object": "invoice",
"amount_due": 1000,
"status": "paid"
// ... resource data
}
}
}
Verifying Signatures
Credibill signs the webhook events it sends to your endpoints by including a signature in each event’s X-Credibill-Signature header. This allows you to verify that the events were sent by Credibill, not by a third party.
Next.js (App Router)
cURL (simulate webhook)
// app/api/webhooks/route.ts
import { NextResponse } from "next/server";
import crypto from "crypto";
export const runtime = "nodejs";
export async function POST(request: Request) {
const raw = await request.text();
const signature = request.headers.get("x-credibill-signature") || "";
const timestamp = request.headers.get("x-credibill-timestamp") || "";
const webhookSecret = process.env.CREDIBILL_WEBHOOK_SECRET || "";
// Construct payload and expected signature
const payload = `${timestamp}.${raw}`;
const expected = crypto
.createHmac("sha256", webhookSecret)
.update(payload)
.digest("hex");
let isValid = false;
try {
isValid = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
} catch (err) {
isValid = false;
}
// Validate signature and timestamp (prevent replay attacks)
const now = Math.floor(Date.now() / 1000);
const ts = parseInt(timestamp || "0", 10);
if (!isValid || Math.abs(now - ts) > 5 * 60) {
return NextResponse.json(
{ error: "Invalid or stale webhook signature" },
{ status: 401 }
);
}
// Parse event and handle idempotently
const event = JSON.parse(raw);
// Idempotency example (pseudo-code): ensure event.id is processed once
// Example with Prisma:
// const existing = await prisma.webhookEvent.findUnique({ where: { id: event.id } });
// if (existing) return NextResponse.json({ received: true });
// await prisma.webhookEvent.create({ data: { id: event.id, type: event.type, payload: raw } });
switch (event.type) {
case "invoice.paid": {
const invoice = event.data.object;
// handle the invoice in an idempotent way
// await fulfillOrder(invoice);
break;
}
default:
console.log(`Unhandled event type ${event.type}`);
}
return NextResponse.json({ received: true });
}
# Simulate webhook delivery (compute signature locally)
export CREDIBILL_WEBHOOK_SECRET="whsec_test_123"
payload='{"id":"evt_123","type":"invoice.paid","data":{"object":{"id":"in_123","amount_due":1000,"status":"paid"}}}'
timestamp=$(date +%s)
signature=$(printf "%s.%s" "$timestamp" "$payload" | openssl dgst -sha256 -hmac "$CREDIBILL_WEBHOOK_SECRET" -hex | sed 's/^.* //')
curl -X POST https://your-app.com/api/webhooks \
-H "Content-Type: application/json" \
-H "X-Credibill-Timestamp: ${timestamp}" \
-H "X-Credibill-Signature: ${signature}" \
-d "$payload"
Verify both the signature and the timestamp (recommended 5-minute tolerance)
and implement idempotent handlers to avoid double-processing. Always perform
webhook verification server-side — do not verify signatures in client-side
code.