Skip to main content

Billing API

Billing operations — plans, checkout, subscription lifecycle, invoices, usage, and the Seal AI Private add-on — exposed through the main API as a proxy to the internal billing service. The dashboard (and any cookie-authenticated client) calls these endpoints; the API validates the session and forwards the request to the billing service with internal trust headers.

Base path: /billing


Access Control

WhoWhat they can call
Anyone (no auth)GET /billing/plans
billing:checkout or billing:manage scopePOST /billing/checkout, POST /billing/coupons/validate (registration-flow tokens carry billing:checkout)
billing:manage scope (org owner) or superadminEverything else

Requests without the required scope receive 403 ("Billing access denied. Requires billing:manage scope."). Org owners get billing:manage automatically via their org role; superadmins always pass. On top of the scope check, the billing service re-validates that the requested org_id belongs to the caller.

If the billing service is unreachable or fails internally, the proxy returns 502 ("Billing service unavailable" / "Billing service error"). Query parameters are whitelisted per endpoint — unknown parameters are silently dropped before forwarding.

Most endpoints take the target organization as an org_id query parameter (GET/lifecycle endpoints) or body field (POST endpoints with a JSON body).


Plans

GET /billing/plans

Public — no authentication. Lists the available pricing plans.

Response (array of plans; prices are in cents):

[
{
"tier": "growth",
"name": "Growth",
"description": "For growing businesses",
"price_monthly": 59900,
"price_yearly": 598800,
"seats": 5,
"accounts": 10,
"data_retention_days": 730,
"api_requests_per_minute": 240,
"features": ["Consentless analytics", "Unlimited reports"],
"is_popular": false
},
{
"tier": "scale",
"name": "Scale",
"description": "For scaling teams",
"price_monthly": 107900,
"price_yearly": 1078800,
"seats": 15,
"accounts": 30,
"data_retention_days": 730,
"api_requests_per_minute": 480,
"features": ["Consentless analytics", "Unlimited reports"],
"is_popular": true
}
]

Overview

GET /billing/overview

Complete billing summary for an organization: subscription, current usage, upcoming invoice, and payment method.

ParameterTypeRequiredDescription
org_idUUIDYesOrganization ID

Response:

{
"subscription": {
"id": "42",
"status": "active",
"plan_tier": "growth",
"billing_interval": "monthly",
"current_period_start": "2025-01-08T00:00:00Z",
"current_period_end": "2025-02-08T00:00:00Z",
"cancel_at_period_end": false,
"trial_end": null
},
"usage": {
"human_events": 1240500,
"agent_events": 88210,
"event_limit": 5000000,
"percentage": 24.8,
"days_remaining": 12,
"is_unlimited": false
},
"upcoming_invoice": null,
"payment_method": {
"brand": "visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2027
}
}

Invoices

GET /billing/invoices

List invoices for an organization.

ParameterTypeRequiredDescription
org_idUUIDYesOrganization ID
limitintNoMax invoices to return
starting_afterstringNoCursor for pagination

Response (array; amounts in cents):

[
{
"id": "in_1QaBcD...",
"number": "SM-2025-0042",
"status": "paid",
"amount_due": 59900,
"amount_paid": 59900,
"currency": "eur",
"created_at": "2025-01-08T00:00:00Z",
"due_date": null,
"paid_at": "2025-01-08T00:05:12Z",
"pdf_url": "https://pay.stripe.com/invoice/.../pdf",
"hosted_invoice_url": "https://invoice.stripe.com/i/..."
}
]

Accounting Email

GET /billing/accounting-email

Get the accounting department email that receives invoice copies.

Response:

{ "accounting_email": "finance@acme.com" }

PUT /billing/accounting-email

Update the accounting email.

Body:

FieldTypeRequiredDescription
emailstring (email)YesNew accounting email

Response: same shape as the GET.


Customer Portal

POST /billing/portal

Create a Stripe customer portal session where the user can manage payment methods and view billing history.

Body:

FieldTypeRequiredDescription
return_urlstringNoURL to return to after the portal (max 500 chars)
org_idstringNoOrganization ID to scope the portal session

Response:

{ "portal_url": "https://billing.stripe.com/p/session/..." }

Subscription

GET /billing/subscription

Current subscription details. Returns null if the org has no subscription.

ParameterTypeRequiredDescription
org_idUUIDYesOrganization ID

Response:

{
"id": "42",
"status": "active",
"plan_tier": "growth",
"billing_interval": "yearly",
"current_period_start": "2025-01-08T00:00:00Z",
"current_period_end": "2026-01-08T00:00:00Z",
"cancel_at_period_end": false,
"trial_end": null
}

POST /billing/subscription/cancel

Schedule cancellation at the end of the current period.

ParameterTypeRequiredDescription
org_idUUIDYesOrganization ID (query parameter)

Response:

{
"success": true,
"message": "Subscription will be canceled at the end of the billing period",
"subscription": {
"id": "42",
"status": "active",
"plan_tier": "growth",
"billing_interval": "monthly",
"current_period_start": "2025-01-08T00:00:00Z",
"current_period_end": "2025-02-08T00:00:00Z",
"cancel_at_period_end": true,
"trial_end": null
},
"cancels_at": "2025-02-08T00:00:00Z"
}

POST /billing/subscription/resume

Undo a scheduled cancellation (only while cancel_at_period_end is true).

ParameterTypeRequiredDescription
org_idUUIDYesOrganization ID (query parameter)

Response:

{
"success": true,
"message": "Subscription resumed",
"subscription": { "cancel_at_period_end": false, "...": "..." }
}

GET /billing/subscription/history

Audit trail of subscription changes (checkouts, cancellations, plan changes, webhook events).

ParameterTypeRequiredDescription
org_idUUIDYesOrganization ID
limitintNoMax entries
offsetintNoPagination offset

Response (array):

[
{
"id": 118,
"subscription_id": 42,
"org_id": "9f6e1a2b-...",
"user_id": 7,
"user_email": "owner@acme.com",
"user_name": "Jane Doe",
"action": "cancel_scheduled",
"source": "dashboard",
"previous_state": { "cancel_at_period_end": false },
"new_state": { "cancel_at_period_end": true },
"success": true,
"error_message": null,
"error_code": null,
"ip_address": "203.0.113.10",
"created_at": "2025-01-15T10:24:00Z"
}
]

Usage

GET /billing/usage/current

Event usage for the current billing period. Returns null if there is no usage record yet.

ParameterTypeRequiredDescription
org_idUUIDYesOrganization ID

Response:

{
"org_id": "9f6e1a2b-...",
"plan_tier": "growth",
"status": "active",
"period_start": "2025-01-08",
"period_end": "2025-02-08",
"human_events": 1240500,
"agent_events": 88210,
"event_limit": 5000000,
"percentage": 24.8,
"days_remaining": 12,
"last_synced_at": "2025-01-27T03:00:00Z",
"is_unlimited": false
}

GET /billing/usage/history

Event usage for past periods.

ParameterTypeRequiredDescription
org_idUUIDYesOrganization ID
limitintNoMax entries

Response (array):

[
{
"period_start": "2024-12-08",
"period_end": "2025-01-08",
"human_events": 4120300,
"agent_events": 210040
}
]

GET /billing/overage-status

Overage history (last 12 periods) and whether the yearly one-time forgiveness is still available.

ParameterTypeRequiredDescription
org_idUUIDYesOrganization ID

Response:

{
"org_id": "9f6e1a2b-...",
"history": [
{
"period_start": "2024-12-01",
"period_end": "2024-12-31",
"plan_tier": "growth",
"event_limit": 5000000,
"total_events": 5430000,
"overage_percent": 8.6,
"is_forgiven": true,
"action_taken": "forgiven"
}
],
"forgiveness_available": false,
"forgiveness_year": 2025
}

Coupons

POST /billing/coupons/validate

Validate a discount code before checkout. Accepts billing:checkout or billing:manage scope (registration-flow tokens can call it).

Body:

FieldTypeRequiredDescription
codestringYesCoupon code (max 100 chars)
plan_tierstringNoPlan the coupon will apply to
billing_intervalstringNomonthly or yearly (for the price preview)

Response (prices in cents):

{
"valid": true,
"coupon": {
"code": "LAUNCH20",
"name": "Launch discount",
"description": null,
"discount_type": "percent",
"discount_value": 20,
"discount_display": "20% off",
"applicable_tiers": null,
"valid_until": "2025-06-30T23:59:59Z",
"duration": "once",
"duration_in_months": null
},
"error": null,
"original_price": 59900,
"discounted_price": 47920,
"savings": 11980
}

Checkout

POST /billing/checkout

Create a Stripe Checkout session for a plan subscription. Accepts billing:checkout or billing:manage scope — the registration flow uses billing:checkout so a brand-new user can subscribe before verifying email.

Body:

FieldTypeRequiredDescription
org_idstringYesOrganization ID to subscribe
plan_tierstringYesgrowth or scale (free is rejected; enterprise is contact-sales)
billing_intervalstringYesmonthly or yearly
success_urlstringNoRedirect after successful payment (max 500 chars)
cancel_urlstringNoRedirect if the user abandons checkout
company_namestringNoCompany name for the invoice
countrystringNoCountry code (VAT)
tax_idstringNoVAT/tax ID
coupon_codestringNoDiscount code

Response:

{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_live_...",
"session_id": "cs_live_a1B2c3..."
}

Returns 409 if the org is billed through Shopify (shopify_subscription_active) — Shopify merchants manage their plan in Shopify Admin.


Seal AI Private Add-on

Billing actions for the Seal AI Private add-on — the platform-hosted, EU-private LLM for chat and LENS. All three require billing:manage (org owner or superadmin); the billing service re-validates org membership and plan eligibility.

POST /billing/seal-ai/subscribe

Activate the add-on on the org's existing Stripe subscription (billing starts on the next invoice, no proration).

Body:

FieldTypeRequiredDescription
org_idstringYesOrganization ID

Response:

{
"status": "active",
"billing_interval": "yearly",
"ends_at": null
}

Errors: 403 for Free/no-subscription orgs (seal_ai_not_available_free), 409 if the plan already includes it (seal_ai_included_in_plan, Scale/Enterprise), if it is already active, or if the org is Shopify-billed.

POST /billing/seal-ai/unsubscribe

Remove the add-on. No refund — access continues until the already-paid period ends.

Body: same as subscribe (org_id).

Response:

{
"status": "cancelled",
"billing_interval": "yearly",
"ends_at": "2026-01-08"
}

POST /billing/seal-ai/token-pack/checkout

One-time Stripe Checkout for extra 5M-token packs (non-expiring). Only orgs entitled to Seal AI Private can buy packs.

Body:

FieldTypeRequiredDescription
org_idstringYesOrganization ID to credit the tokens to
quantityintNoNumber of 5M packs, 1-10 (default 1)

Response:

{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_live_...",
"session_id": "cs_live_x9Y8z7..."
}

See Seal AI Private for pricing, quotas, and how token packs interact with the monthly 5M allowance.


Errors

HTTP CodeCause
403Missing billing:manage (or billing:checkout where accepted) scope, or org access denied
404No subscription / resource for the org
409Conflicting state (Shopify-billed org, add-on already active or included in plan)
502Billing service unreachable or internal billing error

See Also