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:
- Create a hosted checkout
- Create a hosted billing portal link
- Verify an incoming webhook
- 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:
CheckoutResultPortalLinkResultNormalizedWebhookEvent
Normalized webhook events
The normalized webhook surface is intentionally small and stable:
payment.succeededsubscription.activesubscription.cancelledunknown
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