Webhooks
Verify provider webhooks and consume OpenBilling's small normalized event surface.
Webhooks
Webhook normalization is one of the core pieces of OpenBilling. Each provider verifies its own delivery format, then returns a small provider-neutral event shape.
Shared verification input
Both adapters accept the same top-level VerifyWebhookInput contract:
type VerifyWebhookInput = {
payload: string | Uint8Array;
signature?: string;
secret?: string;
headers?: Record<string, string | undefined>;
};Notes:
payloadaccepts the raw request body as a string orUint8Arraysecretcan override the configured webhook secret per callheadersis used for providers that need multiple verification headerssignatureis mainly useful for providers like Stripe that can verify from a single header value
Provider header differences
Stripe:
const event = await stripeBilling.verifyWebhook({
payload: rawBody,
headers: {
'stripe-signature': request.headers.get('stripe-signature') ?? undefined,
},
});Dodo:
const event = await dodoBilling.verifyWebhook({
payload: rawBody,
headers: {
'webhook-id': request.headers.get('webhook-id') ?? undefined,
'webhook-signature': request.headers.get('webhook-signature') ?? undefined,
'webhook-timestamp': request.headers.get('webhook-timestamp') ?? undefined,
},
});Normalized event shapes
subscription.active
{
type: 'subscription.active',
provider: 'stripe',
customerId: 'cus_123',
subscriptionId: 'sub_123',
raw: { ... },
}subscription.cancelled
{
type: 'subscription.cancelled',
provider: 'dodo',
customerId: 'cus_456',
subscriptionId: 'sub_456',
raw: { ... },
}payment.succeeded
{
type: 'payment.succeeded',
provider: 'stripe',
customerId: 'cus_789',
paymentId: 'pi_789',
raw: { ... },
}unknown
{
type: 'unknown',
provider: 'dodo',
raw: { ... },
}Current event mapping
Stripe coverage
| Stripe event | Normalized event |
|---|---|
checkout.session.completed with payment_intent | payment.succeeded |
payment_intent.succeeded | payment.succeeded |
customer.subscription.created with active status | subscription.active |
customer.subscription.updated with active status | subscription.active |
customer.subscription.deleted | subscription.cancelled |
Dodo coverage
| Dodo event | Normalized event |
|---|---|
payment.succeeded | payment.succeeded |
subscription.active | subscription.active |
subscription.cancelled | subscription.cancelled |
Why unknown exists
Unsupported events, or events that do not include the minimum fields needed for safe normalization, should resolve to unknown.
That behavior is intentional:
- Your app can acknowledge the webhook without crashing on an unsupported event
- You can still inspect the original provider payload through
raw - OpenBilling stays honest about the limits of the current MVP
Example handler
import { Payment, Subscription, Webhook } from '@openbilling/core';
switch (event.type) {
case Payment.Succeeded:
await recordPayment(event.paymentId);
break;
case Subscription.Active:
await activateSubscription(event.subscriptionId);
break;
case Subscription.Cancelled:
await cancelSubscription(event.subscriptionId);
break;
case Webhook.Unknown:
console.log('Ignoring unsupported event', event.raw);
break;
}OpenBilling should not fail if the provider sent an event outside the current MVP surface.