Skip to main content

Billing

The Billing API is a thin facade over Stripe. Adjudon does not store card data, does not run its own subscription state machine, and does not bill anyone directly — the four endpoints on this resource read plan metadata from environment variables, mint Stripe Checkout or Customer Portal sessions, and surface the subscription state Stripe webhooks have written back to Organization.billing. The authoritative billing record lives in your Stripe dashboard; /api/v1/billing is the read-and-redirect surface that wires the Adjudon dashboard to it. Tested against API version v1. JWT auth on every endpoint; mutations require org-level owner or admin (checked against OrgMember.role, not the system-level User.role).

What this resource is and is not

The four endpoints on this page are:

  1. GET / — the subscription state Stripe wrote back to the org via webhook plus the live month-to-date trace count.
  2. GET /plans — the plan metadata catalogue (names, prices, features) read from env vars so prices can change without a frontend deploy.
  3. POST /checkout — mints a Stripe Checkout Session with both the base recurring price and the metered overage price as line items.
  4. POST /portal — mints a Stripe Customer Portal session for self-service plan changes, payment-method updates, invoices, and cancellations.

Card data, invoices, dunning, refunds, proration, tax handling, and PSD2/SCA flows are entirely Stripe's responsibility. Adjudon receives state changes via the Stripe webhook at /api/stripe/webhook and writes the resulting plan, status, and currentPeriodEnd to Organization.billing. There is no Adjudon-native invoice object; invoices are surfaced via the Customer Portal redirect.

Endpoints

GET   /api/v1/billing
GET /api/v1/billing/plans
POST /api/v1/billing/checkout
POST /api/v1/billing/portal

The legacy unversioned mount /api/billing returns the same payloads with a Deprecation header; migrate to /api/v1/billing.

Get current billing state

GET /api/v1/billing

Returns the active subscription plus this-month trace usage.

curl
curl https://api.adjudon.com/api/v1/billing \
-H "Authorization: Bearer $ADJUDON_JWT"

Response (200 OK):

{
"success": true,
"data": {
"subscription": {
"plan": "scale",
"status": "active",
"currentPeriodEnd": "2026-06-01T00:00:00.000Z",
"cancelAtPeriodEnd": false,
"stripeCustomerId": "cus_PqJ8r2Yt..."
},
"usage": {
"traces": 48217,
"tracesLimit": 100000
},
"invoices": []
}
}
FieldTypeDescription
subscription.planenumsandbox, scale, governance, enterprise, custom
subscription.statusstringMirror of Stripe's subscription.status (active, past_due, canceled, trialing, …)
subscription.currentPeriodEndstring (ISO 8601)When the current Stripe billing period closes
subscription.cancelAtPeriodEndbooleantrue after the user clicks Cancel in the portal but before period close
subscription.stripeCustomerIdstring | nullStripe cus_* ID; null for Sandbox orgs that never checked out
usage.tracesLimitnumber | nullnull for custom (unlimited)
invoicesarrayReserved. Currently always []; use the Customer Portal for invoice history

Errors: 401, 404 ORG_NOT_FOUND, 500 INTERNAL_ERROR.

List plans

GET /api/v1/billing/plans

Returns the plan catalogue. Prices come from PLAN_PRICE_* env vars so the dashboard can display new pricing without redeploying. The authoritative price lives in Stripe; this catalogue is a display-only mirror.

PlanMonthly limitOverage rateDescription
Sandbox10,000hard blockSolo developers & prototyping
Scale100,000€0.003 / traceEarly-stage teams
Governance500,000€0.001 / traceMid-market compliance
Enterprise2,000,000€0.0005 / traceLarge orgs, advanced AI governance
Customunlimitedcustom contractOn-prem / private cloud

Errors: 401, 500.

Create a Checkout session

POST /api/v1/billing/checkout
Body fieldRequiredDescription
planIdyesOne of scale_monthly, scale_annual, governance_monthly, governance_annual, enterprise_monthly, enterprise_annual

Mints a Stripe Checkout Session pre-configured with two line items: the base recurring price (licensed, quantity 1) and the metered overage price (no quantity — reported asynchronously via stripeService.reportMeteredUsage once ingestion crosses the plan limit). The session's client_reference_id is the Adjudon organisation ID; the webhook handler reads it on checkout.session.completed and stamps stripeCustomerId + subscriptionId on the org.

curl
curl -X POST https://api.adjudon.com/api/v1/billing/checkout \
-H "Authorization: Bearer $ADJUDON_JWT" \
-H "Content-Type: application/json" \
-d '{ "planId": "scale_annual" }'

Response: { "url": "https://checkout.stripe.com/c/pay/cs_live_..." }. Redirect the browser to that URL; do not embed it.

Errors: 400 INVALID_PLAN, 401, 403 BILLING_FORBIDDEN, 404 ORG_NOT_FOUND, 500 CHECKOUT_FAILED.

Open the Customer Portal

POST /api/v1/billing/portal

Mints a Stripe Customer Portal session and returns the redirect URL. The portal handles every subscription mutation Adjudon does not expose directly: payment-method updates, plan changes, cancellations, invoice downloads, and tax-ID management.

curl
curl -X POST https://api.adjudon.com/api/v1/billing/portal \
-H "Authorization: Bearer $ADJUDON_JWT"

Errors: 400 NO_STRIPE_CUSTOMER (org never checked out), 401, 403 BILLING_FORBIDDEN, 404, 500 PORTAL_FAILED.

Common gotchas

  • The org-role gate is its own check. Mutations call requireOrgAdmin, which reads OrgMember.role — the per-org membership role — not the system-level User.role the rest of the API uses. A user who is an admin on the workspace but a member on the organisation cannot start checkout. Surface BILLING_FORBIDDEN clearly in the UI.
  • Overage is metered, not pre-paid. The Checkout Session attaches a metered price line item with no quantity. Adjudon reports usage asynchronously via stripe.billing.meterEvents.create whenever ingestion exceeds the plan limit; Stripe meters it onto the next invoice. There is no API to read the current overage tally on this resource — check the Customer Portal invoice or GET /usage/current for the trace count.
  • invoices is always []. The field is reserved on the response shape but the implementation never populates it. Treat the Customer Portal as the source of truth for invoice history; do not build an invoice list off this field.
  • Sandbox is a stripe-less plan. Sandbox orgs have no Stripe customer record and no period end — stripeCustomerId is null, currentPeriodEnd falls back to the first of next month, and POST /portal returns 400 NO_STRIPE_CUSTOMER. Upgrade flows must go through Checkout, not Portal.
  • Stripe is the source of truth. Adjudon writes plan / status / period-end on the customer.subscription.* and checkout.session.completed webhook events; if a user cancels via the Portal, the cancelAtPeriodEnd flag flips on the next webhook delivery, not on the Portal-redirect call. Build UI states that tolerate a brief window where the cached Adjudon state lags Stripe by a few hundred milliseconds.

See also

  • Usage API — the trace-count meter that drives the metered overage Stripe bills against
  • Plans & Features — the full feature-gate matrix per plan
  • Sub-Processors — Stripe (Ireland) is the payment processor of record under EU SCCs
  • Error Codes — the full error taxonomy