BirrJS
Concepts

Webhooks

Handle payment events in real time with idempotent webhook processing.

BirrJS processes webhooks from your payment provider to update subscription status.

Webhook Events

EventEffect on subscription
charge.successpendingactive. Sets startedAt, calls renewSubscription() to extend expiresAt. Renews if already active.
charge.failedpendingfailed. Active subscriptions are not affected.
charge.cancelledSame as charge.failed
charge.reversedMarks as cancelled
charge.refundedMarks as cancelled
Unknownignored (logged, no status change)

How It Works

1. Receive the webhook

The provider sends a POST request to your configured callback URL (e.g. https://your-app.com/api/handle-webhook).

2. Provider verification

The provider driver validates the webhook signature and returns a normalized event. For Chapa, this involves:

  • Validating the JSON payload structure
  • Verifying HMAC-SHA256 signature from the chapa-signature header
  • Cross-validating against the Chapa verify API

3. Idempotency check

BirrJS checks if this event was already processed using a unique index on (providerId, providerReferenceId):

  • Already completed → skip (duplicate)
  • Previously failed → reset to processing and retry
  • New → insert as processing

4. Process and update

In a single database transaction:

  1. Update the subscription status
  2. Mark the webhook event as completed

If anything fails, the webhook event is marked as failed with the error message.

Webhook Event Records

Each incoming webhook is stored in the webhookEvent table with these statuses:

StatusMeaning
processingBeing processed
completedApplied successfully
failedError during processing
ignoredUnknown event type or no matching subscription

Events are uniquely identified by (providerId, providerReferenceId) — the same event sent twice skips processing.

Configuration

Set the callbackUrl when creating your BirrJS instance. The same path /handle-webhook handles both GET callbacks and POST webhooks:

createBirr({
  provider: chapa({
    callbackUrl: "https://your-app.com/api/birrjs/handle-webhook",
    // ...
  }),
});

GET callback

Chapa sends a GET request to callbackUrl after payment with ?trx_ref=...&ref_id=...&status=success. BirrJS verifies the transaction via the provider and activates the subscription.

POST webhook

Configure the same URL as the Webhook URL in your Chapa dashboard. Chapa sends a POST with the full event payload (signature-verified via webhookSecret).

Both mechanisms can arrive for the same payment. BirrJS handles this gracefully — the subscription is only activated once. The POST webhook uses the webhookEvent table for deduplication; the GET callback checks if the subscription is already active before updating.

The webhook endpoint does not require customer identity. It uses requireHeaders: true and requireRequest: true to access the raw body and headers directly.

Failed Payment Protection

If a renewal payment fails but the current period hasn't expired, the subscription stays active — the customer keeps access until their paid period ends. Only pending subscriptions (never activated) are marked as failed.