This site is being rebuilt and some pages are out of date. For current details, write to reach@inorbit.hr. This notice goes away when the rebuild is done.

No analytics unless you allow it, no tracking. This site keeps in your browser the language you pick, the theme, its colour, which site you chose, the currency on the pricing page and that you closed this notice; signing in adds session cookies. The legal page has the details.

Sign in

← Back to Platform

RFC 0052open2026-10-04

Billing, paid plans through Stripe as the merchant of record

How an account pays for Personal or Team and what that changes. Stripe sells as the merchant of record and holds every card; a billing service of ours keeps Stripe's ids and the subscription, and sets the plan and units on the account only when Stripe's signed event says the payment went through. Team's units past its allowance are reported to Stripe's meter. Enterprise stays an agreement with sales. Test mode until the prices are decided.

Problem

Every account is on Free: 100,000 units a month and calls refused at zero (RFC 0015). The pricing page proposes Personal, Team and Enterprise, with Team serving past its allowance at a price per million units. None of it can be bought. There is no way to take a payment, no record of who paid for what, and nothing that changes an account's plan except the platform admin's hand.

Three things make this harder than a checkout button:

  • Tax and receipts. Selling software to people across the EU means VAT at each buyer's rate, OSS returns and an invoice per payment. A company of one should not run that by hand.
  • Card data. Taking a card number on our own pages puts every page that touches it in PCI DSS scope. The compliance programme keeps card data out of scope by design.
  • Trust in the plan. An account's plan decides what it may call. It must never change because a page said "thank you", only because the payment cleared.

Proposal

Stripe as the merchant of record

Stripe Managed Payments sells the subscription in Stripe's name: Stripe is the merchant of record, collects and remits VAT, and issues the receipts. InOrbit sells to Stripe. This was decided for the platform's first paid product on 2026-09-29 and holds here.

Checkout and the customer portal are Stripe's hosted pages. A card number is typed only on Stripe's pages and never crosses our network, so our part of PCI DSS stays the smallest self-assessment (SAQ A). We do not describe this as compliance; it is where the line is drawn.

The billing service

A new service, billing, owns everything between an account and Stripe:

  • The catalogue. Free, Personal, Team and Enterprise, with their units, seats and behaviour at zero, from the pricing model (docs/pricing/model.json). A test keeps the service's catalogue and the model equal. Enterprise is listed with no price: it is agreed with sales, and its number lives only in the internal pricing room.
  • Checkout. CreateCheckoutSession opens Stripe Checkout for Personal (a person's own account) or Team (a team, at least three seats), monthly or yearly. The answer is Stripe's address. An account that already has a live plan changes it in the portal. The account's Stripe customer is made before its first Checkout, so two sessions opened at once share it and no payment goes to a customer billing does not know.
  • The portal. CreatePortalSession opens Stripe's customer portal: seats, plan, card, invoices, cancel. Team's seats stay between its minimum of three and 10,000.
  • What is kept. Stripe's customer and subscription ids, the plan, interval, seats, status and period end. No card, no address, no amount owed.
  • Invoices. ListInvoices pages Stripe's invoices for the account, newest first, with Stripe's hosted page and PDF.

Every RPC but Stripe's webhook is a person's: an account's own person or a team's owner or admin. No API key reaches billing, so a leaked key can never buy, cancel or read invoices. Billing is admin-only until launch, like every new product.

The plan changes on Stripe's word

Stripe tells billing what happened through signed events: a completed Checkout, a subscription made, changed or deleted, a failed payment. The webhook is the one open route of billing, a deliberate exception to "every route names a token requirement": Stripe has no token. In its place:

  • the body must carry a Stripe-Signature that verifies (HMAC-SHA256 with the endpoint's secret) and is at most five minutes old;
  • each event is handled once, by its id; a redelivery changes nothing;
  • an event says only that a subscription changed: billing reads the subscription from Stripe and applies that, so a late or reordered event never brings back a state that has passed;
  • the route takes POST only, a declared body of at most 256 KiB, and a rate per address and per replica at the edge;
  • it acts for nobody: no identity reaches it.

When the subscription is active, on trial, or late while Stripe retries the card, the account gets its plan; Team also gets 500,000 units a month per seat on top of the plan's 3,000,000. When it is cancelled, unpaid or expired, the account goes back to Free. A first payment still in progress changes nothing. The accounts service applies the change (a new internal call, SetEntitlement, open to the billing service alone) and records it in the account's log like any plan change. An enterprise account's plan is the admin's and is never changed by billing.

A diagram is drawn here in the RFCs product; this page does not show diagrams yet.

A diagram is drawn here in the RFCs product; this page does not show diagrams yet.

Overage

Team serves past its allowance. Every hour one replica reads each Team account's month (the allowance with its seats and grants, and the units used) and reports what is past the allowance and not yet reported to Stripe's meter as one event. The event's identifier names the account, the month and the new total, so a report repeated after a crash counts once. Stripe bills the metered price with the next invoice. Free and Personal stop at zero and are never metered.

Events, audit, erasure

New events: billing.subscription.created, billing.subscription.updated, billing.subscription.canceled and billing.payment.failed, the last also in the notification bell. They carry the plan, seats, status or attempt, never an amount or a person. Every call is audited (who, what, outcome). Erasing an account cancels its subscription and deletes its customer at Stripe, which takes the card and address with it; the export lists the plan and Stripe's ids. Stripe's follow-up events for an erased account change nothing: an ended subscription is recorded only for a customer billing knows. With no billing store configured, erasure answers that nothing is held, so the accounts sweeper never waits on billing.

Test mode first

The prices are proposals. Until they are decided, billing runs against Stripe's test mode and refuses a live key. One open question has to be answered before any price is: at today's rate of 4 units per 1,000 generated tokens, generation costs us more than its units earn (the pricing room shows it). Generation needs its own price or a higher unit rate before Team's overage is sold.

Alternatives considered

Stripe direct, InOrbit as the seller. Lower fees, and the same API. We would file VAT in every EU country through OSS, issue fiscalised invoices in Croatia, and handle reverse charge for business buyers. Refused for a company of one.

Paddle or Lemon Squeezy as merchant of record. Both take VAT off our hands. Stripe won on its metered billing (Team's overage) and its customer portal, and because the first paid product already chose it.

Card fields on our own pages (Stripe Elements). A nicer checkout, and our pages in PCI scope. Refused: card data stays out of scope by design.

Change the plan when Checkout returns. Simpler, and wrong: the return page can be reached without paying. Only Stripe's signed event changes a plan.

Let API keys manage billing. Useful for automation, and a leaked key could then buy seats or cancel a plan. Refused until there is a scope for it and a reason.

Decision

Open. Built in test mode; the prices, Managed Payments' activation and the generation price are decisions for the account owner.

Publication

The service, its contract and its tests land together; the console's billing page uses them. The site's pricing page stays admin-only until the prices are decided. Nothing on the site says plans can be bought before a live key is set.

Status log

  • 2026-10-04: opened. The billing service, its proto, the billing schema, the paid plans on the accounts side (personal, team, enterprise, and seat units through SetEntitlement), the webhook route at the edge, events and erasure built and tested against a recorded Stripe API. Not yet: a run against Stripe's test mode (the test key is the account owner's to provide), Managed Payments activated on the Stripe account, the console's billing page (in its own change), live keys.
  • 2026-10-04: review fixes before the first merge. Subscription events apply Stripe's current state rather than the event's copy; an erased account's late events are ignored; erasure works with no billing store; the customer is made before Checkout; the portal holds Team's seats to 3-10,000; the webhook endpoint is pinned to the API version the service reads. Open: a second live subscription for one account is recorded and logged, and refunding it is done by hand.
  • 2026-10-05: live in Stripe test mode, admin-only. The service (#265) and the console's billing page (#252) are rolled; the test-mode catalogue, overage meter, customer portal and webhook are made in Stripe; a signed event is accepted at the edge and an unsigned one refused. Not yet: Managed Payments activated on the Stripe account, the prices decided, live keys.
  • 2026-10-05: Managed Payments on. Every Checkout session asks Stripe to sell as the merchant of record ([stripe] managed_payments), and each product carries an eligible tax code (SaaS for personal use on Personal, for business use on Team and its overage). The sandbox accepts a Team session with its metered overage price under Managed Payments. Customers see the purchase as sold through Link, and Link sends the receipts.
  • 2026-10-06: seats. A Team's seats are managed in the console without the portal: GetSeats (bought, taken by members and open invitations, free, and the next invoice as Stripe previews it), PreviewSeats (the change with its prorations, before anything is charged) and SetSeats (Stripe's quantity at the previewed time, applied the way the webhook applies it). An invitation needs a free seat: the accounts service keeps the seats with the plan (plan_seats, zero for every account without a Team subscription) and refuses an invitation past them, in words the owner can act on, and the console offers to buy the missing seats where the invitation is made. Billing no longer requires the platform's admin role ([access] require_role is empty): every account's owner or admin uses it; only the console's visibility is gated until launch. A Checkout for Team now sends the seats the card shows (#369).
  • 2026-10-06: seats, after review. Test mode grants nothing by default: a test-mode subscription gives its plan, units and seats only to the accounts in [stripe] test_entitled_accounts (the platform's own team and named test accounts); any other is recorded and shown as "test mode: no plan granted". The prices (ListPlans) are the platform admin's again while test_mode_only holds, as this RFC says. More seats are invoiced and paid at once (always_invoice, error_if_incomplete) before they count; fewer take effect at the period's end through a Stripe subscription schedule, with the seat limit lowered at once in accounts under its lock. A seat change is read back from Stripe and recorded as Stripe has it, with a key of its own; an increase needs a preview from the last day. Stripe's portal keeps the card, the invoices and a cancellation, no seats or plan, so the 500 cap and the in-use rule hold everywhere. Renewing an expired invitation needs a free seat. Reading, previewing and refused or failed changes are audited too.
  • 2026-10-06: the platform admin's billing reads. GetBillingOverview, ListSubscriptions and ListAccountInvoices answer a person with the admin role across every account, gated at the edge and again in the service, and change nothing. A failed payment is now kept, one row per invoice with its attempt count and the amount due, for 13 months, and goes with the account's erasure and export. The overview's MRR is the catalogue's list price (monthly, or yearly over 12, times the seats on a per-seat plan, over active and past-due subscriptions), never what Stripe billed. It also lists the plans whose units differ between billing's catalogue and the accounts plans. The console's admin billing page follows in its own change.
  • 2026-10-06: early-access prices (the owner). Personal 5 a month or 50 a year, Team 8 a seat a month or 80 a year from two seats, Enterprise a pilot by contract; the same figures in euro and dollars. Units a month: Free 20,000, Personal 200,000, Team 100,000 and 100,000 more a seat, Enterprise at most 1,000,000; a generation costs 20 units per 1,000 tokens instead of 4. Every plan stops at zero, so Team's Checkout carries no overage price until metering is proven. Checkout takes a currency, one of the catalogue's (currency on CreateCheckoutSessionRequest, currencies on ListPlans), and each Stripe price carries the dollar figure as a currency option. Per person, at most 300,000 generated tokens a day and 100 API calls a second.
  • 2026-10-07: the early-access prices are decided (the owner): Free; Personal 5 a month or 50 a year; Team 8 a seat a month or 80 a year, from two seats; Enterprise on request; the same figures in euro and dollars, every plan stopping at zero. Selling stays closed until Stripe runs live, after the accountant's answer on business-only marketplace sales, and customer terms exist; the pricing page stays closed until then and keeps its early-access line.
  • 2026-10-07: Checked: billing runs in Stripe test mode at 932c4445 (#265, #375, #386 and their fixes), with the early-access prices (#485) live in billing and accounts. The pricing page (#498) is in console-ui, not in www. #408 is open. Live mode waits on the owner. Open.
  • 2026-10-07: Team starts at one seat (the owner). The pricing model and billing's catalogue say so together, and a test fails when the two differ, so the console's billing page and the site's pricing page show the same minimum and the same prices.
  • 2026-10-07: the agent in the customer's own network is on every plan, Free included (the owner): crawling and indexing run in the customer's agent, never hosted. Nothing in the service ever stopped Free from enrolling one; the pricing model, its page and the console now say so. Agents per account (the owner, the same day): Free 1, Personal 3, Team 10, Enterprise by contract; until the agents service enforces them, every plan holds up to 50.
  • 2026-10-08: RFC 0100 asks the owner (D9) whether an air-gapped install may run on its licence's limits and report counts at renewal (RFC 0091.5) instead of stopping at zero live.

← Back to Platform