Billing
Checkout, portal and changing plans
Buying, upgrading, downgrading and cancelling are all self-serve, and all three run through Stripe so your card details never touch us. An upgrade takes effect on the request that asked for it. A downgrade waits until the period you already paid for runs out.
Buying a plan
A workspace admin starts checkout, picks monthly or annual, and lands on a Stripe hosted page. A card is always collected, even where nothing is charged today.
curl -s -X POST https://api.engramdynamics.org/billing/checkout \
-H "Authorization: Bearer <your key>" \
-H "Content-Type: application/json" \
-d '{"plan": "team", "interval": "monthly"}'
{ "url": "https://checkout.stripe.com/c/pay/cs_live_..." }
Send the customer to that URL. When they finish, Stripe tells us, and the workspace is on the new plan with its new allowance. Business and Enterprise are collected by invoice on terms, so a finance team pays the way it already pays everyone.
Two refusals are worth handling before you ship an integration:
| Status | Error | What it means |
|---|---|---|
| 403 | legal_acceptance_required | A workspace admin has not accepted the current Service Agreement or DPA. The body names the document and carries accept_url, /settings/legal. See Terms and DPA acceptance. |
| 409 | subscription_exists | This workspace already has a plan. Use change-plan below, never a second checkout: a second checkout mints a second subscription and bills twice. |
Changing plan
One call covers both directions, and the answer tells you which one happened.
curl -s -X POST https://api.engramdynamics.org/billing/change-plan \
-H "Authorization: Bearer <your key>" \
-H "Content-Type: application/json" \
-d '{"plan": "business", "interval": "monthly"}'
{
"plan": "business",
"interval": "monthly",
"effective": "immediate",
"effective_at": "2026-09-13T10:04:11Z",
"message": "You are on Business now. The difference is prorated on your next invoice."
}
| Direction | effective | What happens |
|---|---|---|
| Upgrade | immediate | The new allowance applies on this request and the difference for the rest of the period is prorated onto the next invoice. |
| Downgrade | period_end | Scheduled for the end of the period you already paid for. Nothing changes until then, nothing is credited, and effective_at is the date it switches. |
A workspace with no subscription yet gets 409 no_subscription: there is nothing to change, so send it to checkout instead. Changing a paid plan is buying, so the same legal acceptance gate applies.
The billing portal
Everything a customer wants to do to their own subscription lives in Stripe's own portal, so none of it needs a screen from us: update the card, download invoices and receipts, switch plan, or cancel at the end of the period.
curl -s -X POST https://api.engramdynamics.org/billing/portal \
-H "Authorization: Bearer <your key>"
{ "url": "https://billing.stripe.com/p/session/..." }
In the app it is the Manage billing button on the billing page. A plan switch made inside the portal follows the same rule as the API: upgrades apply straight away, downgrades wait for the renewal. A cancellation is a cancel at period end, so the workspace keeps everything it paid for until the date it runs to.
The portal is never gated on legal acceptance. Buying is, managing what you already bought is not, so a customer can always update a card or cancel.
Reading the current state
GET /billing/status is always a 200 and is the one call a dashboard needs. The numeric allowances are written ... here so they have one home, the plan table; the live response carries real numbers.
{
"enabled": true,
"livemode": true,
"plan": "team",
"plan_name": "Team",
"interval": "monthly",
"subscription_status": "active",
"trial_ends_at": null,
"current_period_end": "2026-10-13T00:00:00Z",
"cancel_at_period_end": false,
"checkout_required": false,
"portal_available": true,
"rate_card": { "per_1k_queries_usd": ..., "per_doc_month_usd": ..., "currency": "usd" },
"entitlements": { "plan": "team", "plan_name": "Team", "free": false, "trial": false,
"documents": ..., "queries": ..., "seats": ..., "workspaces": ...,
"ingest_documents_per_day": ..., "ingest_bytes_per_day": ...,
"overage_documents": false, "overage_queries": false,
"upgrade_url": "/billing" }
}
| Field | What to do with it |
|---|---|
rate_card | The meter rates the plan table publishes, returned here so a billing screen can show pricing without a second request. |
livemode | True when the deployment is on a live Stripe account. A test card only works when this is false. |
subscription_status | active, trialing, past_due or canceled. This is our own mirror of Stripe's, and it is what entitlements are enforced against, so a Stripe outage can delay a change but never take access away. |
cancel_at_period_end | True when a cancellation is already scheduled. Show the date from current_period_end rather than treating the workspace as cancelled. |
checkout_required | True when the workspace has to go through checkout before it can do any more work. |
entitlements | The caps actually being enforced right now, in the shape above. Read these rather than assuming the plan's published numbers, because a support override can raise them. The published figures live in the plan table. |
Failed payments and cancellation
A failed payment does not switch the workspace off. Stripe retries the card, and during that window everything keeps working. Past the grace window shown in the plan table the workspace goes read-only: questions and listings and exports keep working, uploads and new workspaces stop, and the refusal is a 402 with past_due and a link to billing.
A cancelled workspace is treated the same way. Listing, exporting and deleting your own documents are never blocked by a subscription gate, and neither are checkout, change-plan or the portal, so leaving with your data and coming back are both always open.
Next
Terms and DPA acceptance covers the agreement behind a purchase. Plans and allowances holds every number.