OpenBilling

Concepts

Understand OpenBilling's shared workflow surface, provider-specific inputs, and webhook normalization model.

Concepts

OpenBilling is intentionally narrow. It abstracts a few workflow-level operations that are common across SaaS billing systems, but it does not try to erase real provider differences.

Shared workflows

The current shared surface is four operations:

  1. Create a hosted checkout
  2. Create a hosted billing portal link
  3. Verify an incoming webhook
  4. Consume a normalized webhook event

Those workflows are exposed through the shared BillingProvider contract.

Honest abstraction boundaries

OpenBilling keeps the app workflow portable, but not every provider detail is portable.

Example:

  • Stripe checkout requires priceId
  • Dodo checkout requires productId

That difference is intentional. OpenBilling does not invent a fake shared catalog model just to make the APIs look symmetrical.

Checkout inputs

The shared checkout input includes both priceId and productId, but each adapter only uses the field it actually supports.

type CreateCheckoutInput = {
  customerId?: string;
  customerEmail?: string;
  productId?: string;
  priceId?: string;
  successUrl: string;
  cancelUrl: string;
  mode: 'payment' | 'subscription';
  metadata?: Record<string, string>;
};

That design keeps the app contract small while preserving provider-specific reality.

Raw escape hatches

OpenBilling returns raw provider payloads where useful:

{
  raw?: unknown;
}

You can use this when you need provider-specific fields that are outside the current MVP abstraction.

raw appears on:

  • CheckoutResult
  • PortalLinkResult
  • NormalizedWebhookEvent

Normalized webhook events

The normalized webhook surface is intentionally small and stable:

  • payment.succeeded
  • subscription.active
  • subscription.cancelled
  • unknown

Unsupported events should resolve to unknown instead of crashing application logic just because the provider sent an event outside the current MVP surface.

createBilling

createBilling is a typed identity helper from @openbilling/core.

It does not add runtime behavior. Its main job is to preserve the full provider type when you build a custom adapter or wrap a provider with extra provider-specific helpers.

import { Provider, Webhook, createBilling } from '@openbilling/core';

const customProvider = createBilling({
  providerName: Provider.Stripe,
  async createCheckout(input) {
    return {
      id: `checkout:${input.mode}`,
      url: input.successUrl,
      provider: Provider.Stripe,
    };
  },
  async createPortalLink(input) {
    return {
      url: input.returnUrl,
      provider: Provider.Stripe,
    };
  },
  async verifyWebhook() {
    return {
      type: Webhook.Unknown,
      provider: Provider.Stripe,
    };
  },
  getDiagnostics() {
    return 'healthy';
  },
});

The shared BillingProvider methods stay intact, and provider-specific members can remain visible to TypeScript.

What is out of scope

OpenBilling does not currently abstract:

  • Invoices
  • Refunds
  • Disputes
  • Taxes
  • Usage billing
  • Seat billing
  • Payout flows
  • Marketplace or Connect-style flows
  • Entitlements

On this page