Subscription billing
The plans and their limits, the Billing page, upgrades charged at once and downgrades scheduled for the renewal, the built-in test checkout and Stripe, trials and renewals, billing emails, invoices as PDF, and what a failed payment or a suspension changes.
For the Full Stack package
Plans and their limits
Every workspace is on one of three monthly plans, in US dollars:
| Plan | Price | Seats | API requests | Storage |
|---|---|---|---|---|
| Starter | $29 a month | 3 | 10,000 | 5 GB |
| Pro | $99 a month | 10 | 100,000 | 20 GB |
| Scale | $249 a month | 30 | 500,000 | 100 GB |
- A workspace created in onboarding starts on a Trial of Starter, for the platform's Trial length setting (
trial_days, 14 days as seeded). The seed puts Forma Studio on Pro, with three paid invoices. - When a trial reaches its end without a paid plan, the workspace becomes Past due from that moment, and the grace period of a failed payment starts (see A failed payment below). The Billing page says "The trial has ended. Choose a paid plan before" the end of the grace period, and the activity trail records "The trial ended without a paid plan".
- Seats are enforced: an invitation, and accepting one, is refused with
seat_limit_reachedonce every seat is taken. - API requests count the calls made with the workspace's API keys, and Storage the project files it holds. Both are shown against the plan and checked before a downgrade; the API does not refuse a call or an upload over the limit. The request count starts again at zero with each new billing period, a trial's included.
The Billing page
Billing, at /en/dashboard/billing, shows the workspace's subscription:
- The plan with its status (Trial, Active, Past due, Ending, Canceled or Suspended), its monthly price and the next date that matters, such as "Renews" or "Trial ends".
- Change plan (Choose a paid plan during a trial, after it has ended, and once the workspace is canceled) and Manage payment method, or Resume subscription while the plan is ending.
- While a downgrade waits for the renewal, "Moves to Starter on" the date, with Keep Pro (the current plan's name) to take it back.
- Usage: API requests, seats and storage against the plan's limits.
- Invoice history, with a Download button on each invoice.
- Billing email, with Edit: "Payment notices go to this address, and it is printed on every invoice." Stripe Checkout fills it in too.
- Cancel subscription, while the plan is on trial, active or past due.
A member who may read billing but not change it sees the same page with "Only the owner and billing members change the plan." in place of the buttons.
Changing plan
Choose a plan
Press Change plan and pick a plan. Under "What changes" the dialog shows what is Charged today and the Next invoice, both from the API. During a trial, after it has ended and once the workspace is canceled, the button is Choose a paid plan, and every plan can be chosen, the current one included ("Your trial plan" during a trial).
An upgrade: charged at once
A dearer plan takes effect at once, and the renewal date stays. On a running Stripe subscription the dialog says "Takes effect at once. The card on file is charged the prorated difference today, and the renewal date stays.", Charged today is Stripe's own figure, and Upgrade and pay now charges it to the payment method on file with no checkout page. On the test checkout it says "Takes effect at once, prorated for the rest of this period." and Continue to payment opens the checkout. A first payment (a trial, an ended trial or a canceled workspace) is charged the plan's full month and starts a new 30-day period: "The paid plan starts at once. The first month is charged in full.", with "and ends the trial" during a trial.
Pay, then come back
The checkout is the built-in test checkout, or Stripe's own page once Stripe is set up. An upgrade charged to the card on file ends at once with "The workspace is on the Pro plan." Back on the Billing page from a checkout it says "Confirming the payment...", then "Payment received. The workspace is on the Pro plan." with the new plan, its limits and a paid invoice. A checkout left unfinished shows "A checkout for the Pro plan was started and not finished." until it is dismissed.
A downgrade: scheduled, nothing charged
A cheaper plan "Takes effect on" the renewal date, "Nothing is charged today.", and Schedule the change records it; the plan panel then says "Moves to Starter on" that date. Keep Pro beside it takes the change back: "The workspace stays on the Pro plan." While seats, API requests or storage are over the smaller plan's limits, the dialog lists each one and refuses with "Bring usage under the new plan's limits first."
A plan bought through a checkout changes only when the payment provider confirms it: from Stripe's signed webhook, or when the Billing page asks the API on the visitor's return and the API reads the session from Stripe itself. The return address alone never changes a plan.
- With Stripe, an upgrade changes the running subscription in place: Stripe invoices the prorated difference at once and charges the payment method on file. The price shown holds for 30 minutes; after that the upgrade is refused ("The price shown is out of date. Check the new price, then confirm again.") and the dialog shows the new figure. A declined card, a bank that wants the payment confirmed, or no payment method on file is refused with a sentence that says which, and the plan stays as it was.
- With Stripe, a downgrade is a subscription schedule: the current period keeps the price that was paid, the renewal bills the smaller price, and nothing is prorated. When that renewal is paid, the workspace moves to the new plan. Keep releases the schedule, and an upgrade in between drops it (and puts it back if the charge fails).
- With the test checkout, the period renews by itself at its end: a paid invoice at the plan's price, the next 30-day period, the usage count back at zero, and a downgrade scheduled for it applied.
The built-in test checkout
With no Stripe key, every plan change goes through the dashboard's own test checkout at /en/dashboard/billing/checkout/<session>. It shows the workspace, the current and the new plan, the amount and who is billed, says "This is the built-in test checkout. It asks for no card and charges nothing.", and offers Test payment and Cancel.
- Test payment changes the plan at once and writes a paid invoice, exactly as a confirmed Stripe payment does. Cancel charges nothing: "Checkout canceled. Nothing was charged."
- Only a member allowed to manage billing in that workspace can open the page.
BILLING_PROVIDER=simulatorin the API's.envkeeps the test checkout even with Stripe keys set.- Manage payment method says "The test checkout keeps no card, so there is no payment method to manage."
Taking real payments with Stripe
Set STRIPE_SECRET_KEY in the API's .env and restart it. The API's log then says "Stripe is set up in test mode." for a sk_test_ key, or that it charges real cards for a sk_live_ key.
- A first payment opens Stripe Checkout in subscription mode for the plan's monthly price, made out to the billing email. Stripe returns the buyer to the Billing page, which confirms the payment with the API before it shows the new plan.
- Nothing has to be created in Stripe first: each plan's price is created the first time it is needed, with a product named after the plan (
Northstack Pro) and the lookup keynorthstack_<plan>_usd_<cents>_month(northstack_pro_usd_9900_month), and found by that key afterwards. A new plan price gets a new key, and subscriptions on the old price keep it. - After that, an upgrade changes the subscription in place and charges the card on file, and a downgrade is scheduled for the renewal, as in Changing plan above. Stripe renews the subscription itself, and its
invoice.paidmoves the workspace to the next period. - If a new Stripe checkout replaces a subscription the workspace still has, the API cancels the old one, with its unused time credited to the customer's next invoice. If Stripe refuses that cancellation, the API's log says so and names the subscription to cancel in the Stripe dashboard.
- Once a workspace has paid through Stripe, Manage payment method opens Stripe's customer portal and returns to the Billing page.
- Cancel subscription tells Stripe to end the subscription at the end of the period, dropping a downgrade scheduled for the renewal, and Resume subscription takes the cancellation back.
- With test keys no money moves: on Stripe's page pay with the card
4242 4242 4242 4242, any future date and any CVC.
The Stripe webhook
Before going live, add a webhook in Stripe on your API's public address and put its signing secret in STRIPE_WEBHOOK_SECRET. The subscriptions and the website's licence orders share this one endpoint.
| Address | Events |
|---|---|
https://<your-api>/api/billing/webhooks/stripe | checkout.session.completed, checkout.session.expired, invoice.paid, invoice.payment_failed, customer.subscription.deleted |
- Every delivery is checked against Stripe's signature first, and a wrong signature is refused with a 400. Each event id is applied once, however often Stripe sends it.
- With
STRIPE_SECRET_KEYset andSTRIPE_WEBHOOK_SECRETempty, the API's log says so when it starts, and every delivery is refused with a 400 whose message namesSTRIPE_WEBHOOK_SECRET. Until it is set, renewals, failed payments and cancellations are not recorded, and a checkout counts only when the buyer comes back to the Billing page. checkout.session.completedchanges the plan.invoice.paidstores Stripe's invoice once, with its amount and the period it pays for, and makes the workspace Active again (or Ending if it was cancelled). An invoice that starts a period moves the renewal date to its end and sets the usage count back to zero, and a renewal applies a downgrade scheduled for it; an upgrade's prorated invoice moves nothing.invoice.payment_failedmarks the workspace Past due, andcustomer.subscription.deletedmarks it Canceled.- On your own computer Stripe cannot reach the API, and a test payment still completes, because the Billing page asks the API to read the session from Stripe when the buyer returns.
Cancelling and resuming
Cancel subscription asks "Cancel the subscription?": the plan stays active until the renewal date, and after that the workspace keeps its data but stops billing. The status becomes Ending, the panel says "Ends" with the date, and Resume subscription keeps the plan after all. A downgrade scheduled for the renewal is dropped, since there is no renewal for it to happen at.
With Stripe, Stripe ends the subscription on that date and its webhook marks the workspace Canceled. On the test checkout the workspace becomes Canceled at the end of the period. A canceled workspace keeps every read, and cannot start new projects, invitations or API keys until it is on a paid plan again: Choose a paid plan buys any plan at its full price, from that day.
Deleting a workspace (its owner, in Settings, Danger zone) ends its Stripe subscription at once, with no credit for the rest of the period. If Stripe cannot be reached, the deletion is refused with a 409, "The subscription could not be canceled with the payment provider, so the workspace was kept. Try again in a few minutes.", so a deleted workspace is never billed.
Invoices
- An invoice, numbered
INV-with the year and a running number, is written each time a plan change is paid, for the amount charged then, at each renewal of the test checkout, and with Stripe for every invoice Stripe reports paid, renewals included, with Stripe's amount and period. A Stripe invoice is stored once, however often it is reported. Invoice history lists the latest 36, with their date, amount and status (Paid, Open or Void). - Download opens the invoice as a PDF: the plan, the period, who is billed and the total. The link is signed and works for five minutes, without signing in, so it can be opened in a new tab.
- The billing email on the page is the address the invoice names and the one Stripe Checkout fills in.
Trials and renewals over time
What the calendar makes due is applied by the API itself, once each: a trial that reaches its end, a test-checkout period that renews, and a subscription set to end with its period. It runs every time a workspace's billing is read, and once an hour for every workspace (the first pass a minute after the API starts), so a server that was off catches up when it comes back.
- Stripe renews its own subscriptions; for those the API follows Stripe's
invoice.paidandinvoice.payment_failed. - Each change is made once even when two requests, or two API servers, settle the same workspace at the same moment.
- The activity trail records an ended trial (
billing.trial_ended), a plan change (billing.plan_changed, including one applied at a renewal) and a scheduled change taken back (billing.schedule_canceled).
Billing emails
Four billing events are mailed to the workspace's owner and billing members, each in their own language, or the workspace's when they have chosen none, and to the billing email in the workspace's language when it is not already one of theirs, with a link to the Billing page:
- a failed payment, with the end of the grace period;
- a trial that ended without a paid plan, with the same date;
- a plan change that was paid;
- a scheduled change applied at the renewal.
The workspace's Email notifications switch (Settings, Notifications) turns these four off for everyone in the workspace. Invitations, password resets and licence receipts are always sent. Without SMTP settings every message is written to the API's log instead.
A failed payment
When Stripe reports a failed payment the workspace becomes Past due, and the Billing page says "The last payment failed. Update the payment method before" the end of its grace period. A trial that ended without a paid plan is past due in the same way, from its end date.
- During the grace period,
BILLING_GRACE_DAYSin the API's.env(7 by default), everything keeps working. - After it, the workspace cannot start new projects, invitations or API keys; the API refuses them with
billing_grace_expired. Every read and every existing record keeps working, and nothing is ever deleted for a failed payment. - The next paid invoice makes the workspace Active again; after an ended trial, the first payment does.
A suspended workspace
Platform staff can suspend a workspace from the platform administration. Its members can still sign in and read everything, every change is refused with workspace_suspended, API keys may only read, and the Billing page says "Suspended. Contact support to restore the workspace."
Who can do what
| Permission | Allows | Seeded roles |
|---|---|---|
billing.read | Opening the Billing page, its invoices and their downloads | Owner, Billing |
billing.manage | Changing plan, keeping the plan instead of a scheduled change, the test checkout, the payment portal, cancelling, resuming and the billing email | Owner, Billing |
Both are in each workspace's roles matrix, so the owner can grant them to another role. A role holding neither gets the workspace without its billing email (billingEmail: null) from every workspace route; platform staff still see it on the organization's page.
The routes behind it
GET /api/workspaces/:workspaceId/billinganswers the subscription, the plans, the usage and any pending checkout;.../billing/invoicesthe latest 36 invoices.POST .../billing/previewwith{ "planId": "pro" }answers what the change costs and when it applies. For an upgrade of a running Stripe subscription it carrieschargesCardOnFile: trueand aquote, valid for 30 minutes.POST .../billing/checkoutwith theplanId(and thatquote) makes the change:upgraded: truewhen the card on file was charged, the page to pay on (url) for a checkout, orscheduled: truefor a downgrade. A downgrade over the limits is a 409over_plan_limitlisting them; an expired quote a 409preview_expired; a refused charge a 402payment_declined,payment_needs_actionorpayment_method_missing.POST .../billing/keep-plantakes back a scheduled downgrade, or answers 409no_scheduled_change.POST .../billing/checkout/:sessionId/syncreads a returned checkout from the provider.POST .../billing/portal,.../billing/canceland.../billing/resume, andPATCH .../billing/email, do what their names say.GET .../billing/invoices/:invoiceId/download-linkanswers a signed link toGET /api/billing/invoices/download?token=..., valid for five minutes.POST /api/billing/webhooks/stripetakes Stripe's events;GET /api/billing/simulator/:sessionIdwithPOST .../completeandPOST .../cancelserve the test checkout.
Selling your own plans
Included with your purchase. Sign in to read, or open it in your download.
Where the plans are defined, and every place to change to sell plans of your own.