Billing
How the hosted service charges, and how to set it up. A self-hosted
deployment without STRIPE_SECRET_KEY has no billing: every organization is
served, whatever its plan.
Plans
| Plan | What it is | Served while |
|---|---|---|
| Trial | One per inbox: the first organization of an account signed up for one, fourteen days, no card, up to ANTIBOT_TRIAL_CALLS billable calls (default 20,000) |
Within the fourteen days and the calls |
| Pro | A Stripe subscription: a monthly fee with calls included, the rest metered | Stripe says the subscription is active, trialing or past_due (through its retries of a failed payment) |
| Enterprise | A contract; set by hand | Always |
A billable call is an identification (browser or app) or an assessment
(the edge's, or your server's). Pulses, challenges, outcomes and the
Management API are not counted. Calls are counted as they are made, each
hour: identifications deleted afterwards (by retention, a purge or an
erasure) are not taken back, and the calls of an environment deleted
afterwards still count against a trial — and, for a subscription, are billed
if their hours had not been reported yet (on the meter on their own, at the
next run) — to that subscription only: one that ends, or another that takes
over, leaves them unbilled. The Management API's usage report (GET /usage)
reads the environments there are: a deleted environment's days leave it,
though its calls stay counted and billed as above.
One free trial per inbox, for good. An inbox is the address as it is
delivered (jane+2@ and [email protected] are one), and it starts one trial:
an account's first organization of its own (an invited member making theirs,
or the one made at sign-up). Every organization an account makes after that
starts without one — served once it subscribes — and an account deleted and
signed up again with the same inbox gets none, in this region or, with a
directory, in any other. An account that has made an organization before
makes no trial, and neither does a throwaway inbox, however it joined. What
is kept for this is a keyed digest of the inbox (trial_claims, and the
directory's directory_trials), never the address: it outlives the account,
and says only that some inbox had its trial, to whoever asks about that
inbox. An Enterprise organization is billed by its agreement: its
owners cannot open Checkout, and no Stripe event makes it Pro.
When an organization is not served, identifications and assessments answer
402 with TrialEnded, TrialCallsUsed or SubscriptionEnded
(errors). The agent reports it and earns no receipt; the edge
and every server SDK read it as the API out of reach and fail open, so the
customer's site keeps serving — unprotected — until an owner subscribes. Data
is kept. The dashboard's Billing page (owners only) says why, and has the
button.
Because nothing on the customer's own pages would show it, a trial's owners
are emailed twice (lib/billing/notices.ts, run by the timers and
/api/cron): three days before it ends, or once 80% of its calls are used,
whichever comes first — and when it has ended. Each is sent once, whatever
the number of instances. Both need mail (RESEND_API_KEY) and
ANTIBOT_PUBLIC_URL for the link to the Billing page.
Setting it up in Stripe
- A product "Pro" with two prices:
- a recurring monthly price —
STRIPE_PRICE_PRO; - a metered price on a meter named
antibot_calls(orSTRIPE_METER_EVENT), aggregated by sum, with graduated tiers whose first tier is free up to the calls a month includes (andANTIBOT_PRO_INCLUDED_CALLSset to the same number, for the dashboard to say it) —STRIPE_PRICE_PRO_METERED. Optional: without it Pro is the flat fee.
- a recurring monthly price —
- A webhook endpoint at
https://<your app>/api/billing/stripe, sendingcheckout.session.completed,customer.subscription.created,customer.subscription.updatedandcustomer.subscription.deleted; its signing secret isSTRIPE_WEBHOOK_SECRET. The endpoint is a 404 without it, and refuses anything not signed with it in the last five minutes. - The customer portal turned on (Settings → Billing → Customer portal): cards, invoices, cancelling. The Manage billing button opens it.
STRIPE_SECRET_KEY, andANTIBOT_PUBLIC_URL(Checkout and the portal send people back to it).
Each instance reports its subscribed organizations' calls to the meter every
ten minutes, an hour at a time for hours that are over (and /api/cron does it
on serverless). Every call is reported — the price's free first tier is what
includes them — and each hour's report carries an identifier, so one reported
twice counts once. An hour is reported fifteen minutes after it ends, once
every instance's counts have reached the database; calls made before a
subscription began — on the trial — are not billed. One organization Stripe
refuses is tried again at the next run, the others reported meanwhile.
Stripe delivers events in no particular order and retries them for days, so
each is applied only if it is newer than the last one applied to the
organization (its created): a late updated: active cannot serve an
organization whose subscription was deleted. An event about a subscription
other than the organization's own is applied only when it brings a live one
to an organization without one, so the late deletion of an old subscription
cannot end the new one. Checkout serves the organization once it is paid; a
payment still on its way waits for its subscription's own event. A second
Checkout is refused while a subscription is live, and each lasts half an hour
(Stripe's shortest); and should two opened before either was paid both be
paid — the first live, or its payment still on its way — the second is not
written over the first: the webhook ends it at once
and credits its unused time to the customer's balance, which the first one's
next invoice uses (answered 500, for Stripe to send it again, if Stripe
cannot be reached to end it). Whichever of a new subscription's events comes
first — Checkout's or the subscription's own — the meter starts from the hour
it began, so a trial's calls are never billed.
What the pricing page shows
/pricing says the trial's calls and Pro's included calls from the same
settings as the limits, and the prices from ANTIBOT_PRICE_PRO_MONTHLY
(default $49) and ANTIBOT_PRICE_PRO_PER_1K (default $0.50): set them to
the prices in Stripe, which are what is charged.
In each region
Each data region is a stack of its own (deployment architecture), with its own webhook endpoint; the subscriptions of its organizations name it. One Stripe account serves every region: set the same prices in each stack.
What is not built
Changing the plan by hand (to Enterprise) is a database update for now; invoices, tax and dunning emails are Stripe's.