Invoices and Billing Portal
This page covers reading a customer’s invoices, downloading an invoice PDF, and opening the provider-hosted billing portal. All three depend on optional provider capabilities and degrade or fail explicitly when the provider does not support them.
Listing invoices
ListInvoicesAction returns the customer’s invoices from the provider.
const invoices = await new ListInvoicesAction(deps).handle(billable, 50);
handle(billable, limit?):
- Requires the provider to be invoice capable (
isInvoiceCapable, i.e. it implements bothlistInvoicesanddownloadInvoicePdf); otherwise throwsProviderCapabilityNotSupportedError(reported as theinvoicePdfcapability). - If there is no storage driver, returns
[]. - Loads the local customer row; if it is missing or has no
providerCustomerId, returns[]. - Calls
provider.listInvoices({ providerCustomerId, limit }).
Output: InvoiceDTO[]:
export interface InvoiceDTO {
providerInvoiceId: string;
status: InvoiceStatus;
total: Money;
hostedInvoiceUrl: string | null;
invoicePdf: string | null;
}
total is a Money value object; hostedInvoiceUrl and invoicePdf are provider-hosted links when
available.
Downloading an invoice PDF
DownloadInvoicePdfAction fetches the raw PDF bytes for one invoice.
const pdf = await new DownloadInvoicePdfAction(deps).handle('in_1');
// pdf.filename -> 'in_1.pdf', pdf.content -> Uint8Array
handle(providerInvoiceId):
- Requires the provider to be invoice capable; otherwise throws
ProviderCapabilityNotSupportedError. - Calls
provider.downloadInvoicePdf(providerInvoiceId).
Output: InvoicePdfDTO:
export interface InvoicePdfDTO {
filename: string;
content: Uint8Array;
}
The action takes the provider invoice id directly and does not touch storage. The application is responsible for confirming the invoice belongs to the right customer before serving the bytes.
Billing portal
payable.customer(billable).billingPortal(returnUrl) returns a provider-hosted portal URL where the
customer can manage payment methods, invoices, and subscriptions.
const { url } = await payable
.customer(billable)
.billingPortal('https://app.test/account');
return redirect(url);
billingPortal(returnUrl):
- Asserts the provider’s
billingPortalcapability viaassertProviderCapability. - Syncs the customer to the provider (
SyncCustomerWithProviderAction) to obtain theproviderCustomerId. - Builds an idempotency key
portal:${providerName}:${billableType}:${billableId}. - Calls
provider.billingPortal({ providerCustomerId, returnUrl }, ctx).
Output: BillingPortalDTO:
export interface BillingPortalDTO {
url: string;
}
sequenceDiagram
participant App
participant Ctx as CustomerContext
participant Sync as SyncCustomerWithProviderAction
participant Provider
App->>Ctx: billingPortal(returnUrl)
Ctx->>Ctx: assert billingPortal capability
Ctx->>Sync: handle(billable)
Sync-->>Ctx: providerCustomerId
Ctx->>Provider: billingPortal({ providerCustomerId, returnUrl }, ctx)
Provider-->>App: BillingPortalDTO { url }
Provider dependency and capabilities
These features ride on optional provider methods declared as capability interfaces on the
PaymentProvider contract:
- Invoices.
InvoiceCapable(listInvoices,downloadInvoicePdf). Detected withisInvoiceCapable. The capability surfaced in errors isinvoicePdf. - Billing portal.
billingPortalis a required method on thePaymentProvidercontract, but its availability is gated by thebillingPortalcapability flag, asserted before use.
The ProviderCapabilities flags (checkout, subscriptions, trials, refunds, coupons,
billingPortal, meteredBilling, invoicePdf) let the application probe support before calling -
see 17-providers.
Inputs and outputs
| Operation | Input | Output |
|---|---|---|
| List invoices | Billable, optional limit | InvoiceDTO[] |
| Download PDF | providerInvoiceId | InvoicePdfDTO ({ filename, content }) |
| Billing portal | returnUrl | BillingPortalDTO ({ url }) |
Edge cases
- Provider lacks invoice capability. Both invoice actions throw
ProviderCapabilityNotSupportedError(invoicePdf). - Provider lacks the billing-portal capability.
billingPortal()throws viaassertProviderCapabilitybefore any sync or provider call. - No storage driver (invoices).
ListInvoicesActionreturns[]instead of throwing. - No local customer / unmapped customer (invoices). Returns
[]. - Billing portal without storage. The portal still syncs the customer via the provider, which
requires no storage to obtain a
providerCustomerId, but nothing is persisted - see 08-customers-billable. - PDF ownership.
DownloadInvoicePdfActiontrusts the supplied provider invoice id; the caller must verify ownership.