Billing API
Opbox billing is database-first. The local BillingSubscription record is the source of truth for status, amount, and period. Stripe is the payment rail only. Every workspace has zero or one subscription with associated line items and an append-only event log.
Three underlying data models back the API:
BillingSubscription- one per workspace, tracks status, collection mode, billing period, amount, and Stripe references.BillingLineItem- recurring or one-off charges attached to a subscription.BillingEvent- immutable audit log keyed onstripeEventIdfor webhook idempotency.
All client-facing routes in this reference require an authenticated session and the OWNER or ADMIN role on the active workspace. Admin tooling for super-org operators (create, update, cancel across any client workspace) lives under /api/admin/billing and is documented separately.
Access
| Method | Endpoint | Description |
|---|---|---|
GET | /api/billing/access | Returns whether the current session is a super-org admin. The billing settings page uses this to choose between the admin overview and the client view. |
Response:
{ "isAdmin": false }
Subscription
| Method | Endpoint | Description |
|---|---|---|
GET | /api/billing/subscription | Load the active workspace's subscription, including the payment method pulled fresh from Stripe. |
GET /api/billing/subscription
{
"id": "cln_sub_abc123",
"status": "ACTIVE",
"collectionMode": "AUTO_CHARGE",
"billingPeriod": "MONTHLY",
"amountCents": 49900,
"currency": "usd",
"currentPeriodStart": "2026-04-01T00:00:00.000Z",
"currentPeriodEnd": "2026-05-01T00:00:00.000Z",
"cancelAtPeriodEnd": false,
"isManual": false,
"paymentMethod": {
"brand": "visa",
"last4": "4242",
"expMonth": 12,
"expYear": 2028
}
}
Status values: INACTIVE, ACTIVE, PAST_DUE, CANCELED, TRIALING.
Collection modes: AUTO_CHARGE, SEND_INVOICE.
Billing periods: MONTHLY, QUARTERLY, ANNUAL.
When isManual is true, the subscription is tracked locally without any Stripe record. Payment method, invoices, and portal access are unavailable for manual subscriptions.
Invoices
| Method | Endpoint | Description |
|---|---|---|
GET | /api/billing/invoices | Last 24 Stripe invoices for the active workspace. Returns an empty array for manual subscriptions. |
Response:
{
"invoices": [
{
"id": "in_1PqR2sABC...",
"number": "ACME-0012",
"amountPaid": 49900,
"amountDue": 0,
"currency": "usd",
"status": "paid",
"hostedInvoiceUrl": "https://invoice.stripe.com/i/acct_.../inv_...",
"invoicePdf": "https://pay.stripe.com/invoice/acct_.../pdf",
"periodStart": "2026-03-01T00:00:00.000Z",
"periodEnd": "2026-04-01T00:00:00.000Z",
"paidAt": "2026-03-01T00:03:12.000Z"
}
]
}
Follow hostedInvoiceUrl for Stripe's hosted invoice page or invoicePdf for the direct PDF download.
Line Items
| Method | Endpoint | Description |
|---|---|---|
GET | /api/billing/line-items | List recurring and one-off charges attached to the active workspace's subscription. |
Response:
{
"lineItems": [
{
"id": "cln_li_onboarding",
"description": "Onboarding setup fee",
"type": "ONE_OFF",
"amountCents": 150000,
"currency": "usd",
"createdAt": "2026-02-10T14:00:00.000Z"
},
{
"id": "cln_li_csp_addon",
"description": "CSP addon - per entity",
"type": "RECURRING",
"amountCents": 5000,
"currency": "usd",
"createdAt": "2026-03-01T00:00:00.000Z"
}
]
}
ONE_OFFitems post as a StripeInvoiceItemagainst the next invoice.RECURRINGitems post as a StripeSubscriptionItemand bill every period alongside the base amount.
Customer Portal
Stripe's hosted customer portal handles card updates, invoice history, and cancellations with Stripe branding.
| Method | Endpoint | Description |
|---|---|---|
POST | /api/billing/portal | Create a short-lived portal session and return its URL. Redirect the browser there. |
POST /api/billing/portal
x-csrf-token: <token>
Response:
{
"url": "https://billing.stripe.com/session/BLI_abc123..."
}
The portal URL expires quickly - request a fresh one per redirect. Manual subscriptions reject this call with 400 Bad Request.
Webhooks
Stripe pushes subscription and invoice events to Opbox so the local record stays in sync. This endpoint uses Stripe's signature verification in place of session auth and CSRF.
| Method | Endpoint | Description |
|---|---|---|
POST | /api/billing/webhooks | Stripe webhook receiver. Raw body required for signature verification. |
Configure in the Stripe dashboard:
- Endpoint:
POST https://opbox.app/api/billing/webhooks - Signing secret: store as
STRIPE_WEBHOOK_SECRET.
Required events:
| Stripe event | Effect on the local record |
|---|---|
invoice.payment_succeeded | Sets status to ACTIVE. Appends an INVOICE_PAID event. |
invoice.payment_failed | Sets status to PAST_DUE. Appends an INVOICE_FAILED event. |
customer.subscription.deleted | Sets status to CANCELED. |
invoice.finalized | Re-syncs the current period from the latest SubscriptionItem. |
Idempotency is enforced by checking BillingEvent.stripeEventId before processing. Duplicate deliveries of the same Stripe event ID are silent no-ops, so safe retry delivery can be enabled without risk of double-charging.
Invalid signatures return 400 Bad Request.
Example: client walkthrough
# 1. Pull the current subscription
curl https://opbox.app/api/billing/subscription \
-b "session=$SESSION"
# 2. List recent invoices
curl https://opbox.app/api/billing/invoices \
-b "session=$SESSION"
# 3. List attached line items
curl https://opbox.app/api/billing/line-items \
-b "session=$SESSION"
# 4. Launch the Stripe Customer Portal
curl -X POST https://opbox.app/api/billing/portal \
-H "x-csrf-token: $CSRF" \
-b "session=$SESSION"
# { "url": "https://billing.stripe.com/session/..." }
# -> redirect the browser to that URL
Status Codes
| Status | Meaning |
|---|---|
200 OK | Successful read. |
201 Created | Subscription or line item created via the admin surface. |
400 Bad Request | Invalid input, invalid Stripe signature on the webhook, or manual-subscription action attempted on a Stripe-only endpoint (portal). |
401 Unauthorized | No active session on a client route. |
403 Forbidden | Session lacks OWNER or ADMIN role. |
404 Not Found | No subscription exists for the active workspace. |
409 Conflict | Subscription already exists when creating via the admin surface. |
500 Internal Server Error | Stripe API failure or unexpected error. Local records remain consistent; retry is safe. |