OpenBilling

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:

  • payload accepts the raw request body as a string or Uint8Array
  • secret can override the configured webhook secret per call
  • headers is used for providers that need multiple verification headers
  • signature is 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 eventNormalized event
checkout.session.completed with payment_intentpayment.succeeded
payment_intent.succeededpayment.succeeded
customer.subscription.created with active statussubscription.active
customer.subscription.updated with active statussubscription.active
customer.subscription.deletedsubscription.cancelled

Dodo coverage

Dodo eventNormalized event
payment.succeededpayment.succeeded
subscription.activesubscription.active
subscription.cancelledsubscription.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.

On this page