Skip to main content
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

  1. Go to the Webhooks section in your Dashboard. This is found under Settings → Webhooks.
  2. Click Add Webhook.
  3. Enter the URL of your server endpoint (e.g., https://api.mycoolapp.com/webhooks/credibill).
  4. 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.
// 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 });
}
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.