Billing API
The Billing API exposes the JWT-authenticated endpoints the dashboard uses to upgrade, downgrade, view invoices, and open the Stripe Customer Portal.
Base URL: https://api.zeridion.com/platform/v1
Every endpoint below requires a valid JWT obtained from POST /platform/v1/auth/login.
Stripe-only. Zeridion uses Stripe for subscription management, invoicing, and Stripe Tax. There is no second billing backend — every monetary action below ultimately resolves to a Stripe API call.
Billing state model
The server tracks subscription health on every project as a BillingState enum:
| Value | Meaning |
|---|---|
active | Subscription is in good standing — quota and rate limits enforced as normal. |
past_due | Most recent invoice failed; Stripe is still retrying. Existing in-flight jobs continue to be processed by workers, but new job creates are blocked unconditionally and return 402 billing_state_blocked until the invoice is paid (any billing state other than active is gated, regardless of plan tier). |
unpaid | Stripe gave up retrying; subscription is suspended. New job creates return 402 billing_state_blocked. |
cancelled | Subscription was cancelled (by the user or by Stripe). Plan reverts to free on the next state-update webhook. |
Trialing collapses to
active. Stripetrialingsubscriptions are deliberately mapped toactiveserver-side — there is no separatetrialingbilling state. A trialing tenant gets the same quota and 402-gate treatment as a paying tenant. To differentiate "still on trial" in the UI, call the Stripe Subscriptions API directly with the project'sstripe_subscription_id(returned byGET /platform/v1/billing/status) and read thetrial_endfield on the subscription object.GET /platform/v1/billing/statusdoes not itself return a trial-end timestamp, and the Stripe Customer Portal is a hosted UI that does not expose one programmatically.
State transitions
active is the only state that admits new job creates. Every other state returns 402 billing_state_blocked on POST /flare/v1/jobs and any other create-shaped endpoint. Existing in-flight jobs continue processing in all states except cancelled.
POST /platform/v1/billing/checkout
Create a Stripe Checkout Session for a plan upgrade and return its hosted-page URL. The user's browser is expected to redirect to the returned checkout_url to complete payment.
Authentication: JWT required (Authorization: Bearer <jwt> or the zf_jwt cookie).
The server derives a per-call idempotency key from {userId}:checkout:{epochMinute} so two rapid clicks within the same minute collapse to a single Stripe session rather than two duplicate ones.
Request
POST /platform/v1/billing/checkout
Authorization: Bearer <jwt_token>
Content-Type: application/json
Body
| Field | Type | Required | Description |
|---|---|---|---|
plan_id | string | Yes | Target plan slug. One of starter, pro, or business. |
promo_code | string | No | Stripe promotion code to apply (case-insensitive). Forwarded verbatim to Stripe; both percentage and fixed-amount coupons are supported. |
{
"plan_id": "starter",
"promo_code": "LAUNCH50"
}
Response
200 OK
{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_a1b2...",
"session_id": "cs_test_a1b2..."
}
| Field | Type | Description |
|---|---|---|
checkout_url | string | Hosted Stripe Checkout URL. Redirect the user here. |
session_id | string | Stripe Checkout Session ID. Useful for diagnostics. |
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_plan | plan_id is missing or not one of starter, pro, business. |
| 400 | invalid_promo_code | promo_code is not active in Stripe or has expired. |
| 401 | unauthorized | Missing or invalid JWT. |
POST /platform/v1/billing/portal
Create a Stripe Customer Portal session so the user can update their payment method, cancel, or download invoices and tax receipts directly from Stripe's hosted UI.
Authentication: JWT required.
Request
POST /platform/v1/billing/portal
Authorization: Bearer <jwt_token>
No request body.
Response
200 OK
{
"portal_url": "https://billing.stripe.com/p/session/test_..."
}
| Field | Type | Description |
|---|---|---|
portal_url | string | Hosted Stripe Customer Portal URL. Redirect the user here. |
Errors
| Status | Code | When |
|---|---|---|
| 400 | no_subscription | The authenticated user has no Stripe customer ID yet (never reached checkout). |
| 401 | unauthorized | Missing or invalid JWT. |
GET /platform/v1/billing/status
Return the current plan, payment-method status, plan limits, and today's usage counters for the authenticated user.
Authentication: JWT required.
Request
GET /platform/v1/billing/status
Authorization: Bearer <jwt_token>
Response
200 OK
{
"plan": "starter",
"currency": "usd",
"stripe_subscription_id": "sub_1NXY...",
"stripe_customer_id": "cus_NXY...",
"limits": {
"max_jobs_per_day": 10000,
"max_projects": 3,
"rate_limit_per_hour": 5000
},
"usage": {
"jobs_today": 142,
"project_count": 2
}
}
| Field | Type | Description |
|---|---|---|
plan | string | Active plan slug: free, starter, pro, business. |
currency | string | ISO-4217 currency code for the active plan (currently always "usd"). |
stripe_subscription_id | string | null | Stripe Subscription ID, or null when on the free plan. |
stripe_customer_id | string | null | Stripe Customer ID, or null if checkout has never been completed. |
limits.max_jobs_per_day | integer | Daily job-create quota for the plan. |
limits.max_projects | integer | Maximum project count allowed. Returns -1 for unlimited tiers (pro, business) — treat any negative value as "no project cap". |
limits.rate_limit_per_hour | integer | Per-tenant request rate limit. |
usage.jobs_today | integer | Jobs created so far today (UTC) across all of the user's projects. |
usage.project_count | integer | Current number of projects owned by the user. |
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | Missing or invalid JWT. |
GET /platform/v1/billing/invoices
Return the most recent invoices for the authenticated user, ordered newest first. Backed by the Stripe Invoices list API.
Authentication: JWT required.
Request
GET /platform/v1/billing/invoices?limit=20
Authorization: Bearer <jwt_token>
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Number of invoices to return. Must be between 1 and 100 inclusive. |
Response
200 OK
{
"data": [
{
"id": "in_1NXY...",
"status": "paid",
"amount_total": 2900,
"currency": "usd",
"stripe_subscription_id": "sub_1NXY...",
"pdf_url": "https://pay.stripe.com/invoice/.../pdf",
"hosted_invoice_url": "https://invoice.stripe.com/i/...",
"created_at": "2026-04-01T12:00:00Z"
}
]
}
| Field | Type | Description |
|---|---|---|
id | string | Stripe Invoice ID. |
status | string | Stripe invoice status: draft, open, paid, uncollectible, void. |
amount_total | integer | Total amount charged in the currency's smallest unit (e.g. cents). |
currency | string | ISO-4217 currency code for the invoice. |
stripe_subscription_id | string | null | The subscription this invoice belongs to. |
pdf_url | string | null | Direct download link for the PDF receipt. |
hosted_invoice_url | string | null | Stripe-hosted HTML view of the invoice. |
created_at | string (ISO 8601) | When the invoice was created in Stripe. |
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_limit | limit is explicitly out of the 1..100 range. |
| 401 | unauthorized | Missing or invalid JWT. |
Plans
| Plan | Price/mo (USD) | Jobs/month | Opt-in overage | Projects | Rate limit/hr |
|---|---|---|---|---|---|
| Free | $0 | 50,000 | — | 1 | 1,000 |
| Starter | $29 | 300,000 | $1.50 / 10k jobs | 3 | 5,000 |
| Pro | $99 | 3,000,000 | $1.00 / 10k jobs | Unlimited | 50,000 |
| Business | $299 | 30,000,000 | $0.50 / 10k jobs | Unlimited | 500,000 |
| Enterprise | Custom | Custom | Custom | Unlimited | Custom |
Job allowances are monthly, aligned to your billing period. When the
allowance is reached, new job creates return 429 monthly_allowance_exceeded
— unless opt-in overage is enabled for the project, in which case jobs
continue and the extra volume is metered at the plan's per-10k rate. Set a
spend cap to bound monthly overage charges; once the cap is reached, new
job creates return 429 spend_cap_reached until the next period.
Tax
VAT and sales tax are collected automatically for EU and UK customers via Stripe Tax. No configuration is required on your side — Stripe determines the applicable rate from the billing address you enter during checkout.
- VAT receipts — every invoice includes a full tax breakdown line. Download the PDF receipt from the Stripe Customer Portal (
POST /platform/v1/billing/portal). - Tax IDs — VAT-registered organisations can enter their VAT/GST number at checkout. Stripe validates the ID in real time and applies the zero-rate B2B rule (reverse charge) where applicable.
- Supported regions — EU member states, United Kingdom, Australia, and any other jurisdictions covered by Stripe Tax.
Currency
The currency field on GET /platform/v1/billing/status returns the ISO-4217 code for the active plan. All plans currently bill in usd. Dashboard price displays read this field so future multi-currency support requires no frontend change.
See also
- Account API — read the authenticated user's account and delete-account flow
- Errors —
402 payment_required,subscription_inactive, and other billing-gated failures - Rate Limits — quota tiers correspond to billing plans