Opbox

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 on stripeEventId for 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

MethodEndpointDescription
GET/api/billing/accessReturns 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

MethodEndpointDescription
GET/api/billing/subscriptionLoad 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

MethodEndpointDescription
GET/api/billing/invoicesLast 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

MethodEndpointDescription
GET/api/billing/line-itemsList 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_OFF items post as a Stripe InvoiceItem against the next invoice.
  • RECURRING items post as a Stripe SubscriptionItem and bill every period alongside the base amount.

Customer Portal

Stripe's hosted customer portal handles card updates, invoice history, and cancellations with Stripe branding.

MethodEndpointDescription
POST/api/billing/portalCreate 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.

MethodEndpointDescription
POST/api/billing/webhooksStripe 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 eventEffect on the local record
invoice.payment_succeededSets status to ACTIVE. Appends an INVOICE_PAID event.
invoice.payment_failedSets status to PAST_DUE. Appends an INVOICE_FAILED event.
customer.subscription.deletedSets status to CANCELED.
invoice.finalizedRe-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

StatusMeaning
200 OKSuccessful read.
201 CreatedSubscription or line item created via the admin surface.
400 Bad RequestInvalid input, invalid Stripe signature on the webhook, or manual-subscription action attempted on a Stripe-only endpoint (portal).
401 UnauthorizedNo active session on a client route.
403 ForbiddenSession lacks OWNER or ADMIN role.
404 Not FoundNo subscription exists for the active workspace.
409 ConflictSubscription already exists when creating via the admin surface.
500 Internal Server ErrorStripe API failure or unexpected error. Local records remain consistent; retry is safe.