Reference
Billing
Everything commercial is read-only over the API. You can see what you are on, what you have paid and what was invoiced; nothing reachable with an API key can change a plan or spend money.
What can read this
billing:read cannot be granted to an API key — it belongs to a signed-in session. These endpoints therefore accept either billing:read or account:read, which is how a server-side key reaches them.
Changing a plan, starting a checkout and cancelling are session-only and are not part of the public API. A leaked server-side key should be able to send mail, not run up a bill.
Current state
/v1/billingaccount:readcurl https://api.posthastemail.dev/v1/billing \
-H "authorization: Bearer $POSTHASTE_KEY"
# 200
# {
# "plan": { "id": "starter", "name": "Starter", "priceUsd": 9 },
# "subscription": {
# "id": "sub_Vd1Lr6Ta0YhJ3mWcQp4Ns8",
# "plan": "starter",
# "status": "active",
# "currency": "USD",
# "amountMinor": 900,
# "interval": "month",
# "currentPeriodEnd": "2026-09-01T00:00:00.000Z",
# "graceEndsAt": null,
# "cancelAtPeriodEnd": false,
# "checkoutUrl": null
# },
# "profile": { "legalName": "Acme Ltd", "billingEmail": "[email protected]",
# "country": "GB", "taxIdKind": null, "taxId": null,
# "currency": "USD" },
# "payments": [ { "id": "pay_…", "kind": "subscription", "status": "captured",
# "currency": "USD", "amountMinor": 900, "taxAmountMinor": 0,
# "method": "card", "paidAt": "…", "createdAt": "…",
# "failureReason": null } ],
# "plans": [ … every plan, with charge and charges by currency … ],
# "configured": true
# }| Field | Notes |
|---|---|
| plan | The plan in force: { id, name, priceUsd }. priceUsd is the one published price and is identical for everyone. |
| subscription | null when there is none. status is one of incomplete, active, past_due or paused. checkoutUrl is present only while incomplete — it is where payment was expected, and is cleared the moment it lands. |
| profile | The billing identity: legal name, billing email, country, tax id. null until it has been filled in. |
| payments | The twenty most recent, newest first, with status, amountMinor, taxAmountMinor, method, paidAt and failureReason. |
| plans | Every plan with its published priceUsd, its entitlements, and charge — what this account would actually be billed, split into base and tax — plus charges keyed by currency so a country selector can reprice without a round trip. |
| configured | Whether a payment provider is configured on this deployment at all. |
Money is always in minor units — 900 with "currency": "USD" is $9.00. Integers, never floats, so nothing rounds on the way through JSON.
The commercial history
/v1/billing/historyaccount:readAn append-only, hash-chained ledger of everything that has happened commercially on the account, oldest first, up to 100 events. It is exposed for the same reason /v1/account/verify is: a tamper-evident record nobody can inspect is just a record.
curl https://api.posthastemail.dev/v1/billing/history \
-H "authorization: Bearer $POSTHASTE_KEY"
# 200
# {
# "chain": { "valid": true, "brokenAt": null, "reason": null },
# "data": [
# { "seq": 1, "type": "subscription_created", "actor": "user:usr_…",
# "reason": null, "detail": { "plan": "starter" },
# "occurredAt": "2026-07-01T09:14:02.551Z", "hash": "3b91…" },
# { "seq": 2, "type": "payment_succeeded", "actor": "provider:razorpay",
# "reason": null, "detail": { "amountMinor": 900, "currency": "USD" },
# "occurredAt": "2026-07-01T09:14:31.207Z", "hash": "c0af…" }
# ]
# }| Field | Notes |
|---|---|
| chain | { valid, brokenAt, reason } — the whole ledger replayed and verified on this read. |
| data[].type | One of plan_changed, subscription_created, subscription_activated, subscription_past_due, subscription_cancelled, payment_succeeded, payment_failed. |
| data[].actor | Who caused it — a user, the payment provider, or the system. |
| data[].reason | Free text where one was recorded, otherwise null. |
| data[].detail | The event’s own payload. Shape varies by type. |
| data[].hash | The chain link. Each entry commits to the one before it. |
Invoices
/v1/billing/invoicesaccount:read/v1/billing/invoices/:idaccount:readAn invoice is issued in the same transaction as the payment it records, so a customer’s receipt exists the instant their money does. Nothing reachable from the API can create, alter or delete one — these two routes exist to hand you a document you already own.
curl https://api.posthastemail.dev/v1/billing/invoices \
-H "authorization: Bearer $POSTHASTE_KEY"
# 200
# {
# "data": [
# {
# "id": "inv_Tz6Kd93Lm2PqR7tUv5WxY1",
# "number": "PH-2026-27-0001",
# "issuedAt": "2026-08-01T09:14:31.207Z",
# "currency": "USD",
# "subtotalMinor": 900,
# "taxMinor": 0,
# "taxRateBp": null,
# "totalMinor": 900,
# "paidMinor": 900,
# "plan": "starter",
# "periodStart": "2026-08-01T00:00:00.000Z",
# "periodEnd": "2026-09-01T00:00:00.000Z",
# "buyer": { "name": "Acme Ltd", "email": "[email protected]",
# "country": "GB", "taxId": null },
# "payment": { "method": "card", "reference": "pay_QxT2Sf9pXbNc7H",
# "paidAt": "2026-08-01T09:14:31.207Z" }
# }
# ]
# }The list returns the 100 most recent, newest first, with no cursor — see pagination. Both routes return the identical object, so a list row never disagrees with the document it links to.
| Field | Notes |
|---|---|
| id | The opaque handle, inv_ prefixed. Use this in the URL. |
| number | The human-facing number, PH-2026-27-0001 — a gapless per-financial-year series. This is what your accountant quotes. It is deliberately not the id: addressing invoices by the number would make every invoice we have ever issued enumerable. |
| issuedAt | When the document was raised. |
| currency | ISO 4217. All amounts below are minor units of it. |
| subtotalMinor / taxMinor / totalMinor | Base, tax and the sum. taxMinor is 0 where no tax was charged. |
| taxRateBp | The rate applied, in basis points (1800 = 18%), or null when no tax applied. |
| paidMinor | What was actually received against it. |
| plan / periodStart / periodEnd | What was bought, and the service period it covers. |
| buyer | { name, email, country, taxId }, frozen as they were when the invoice was raised — later edits to your billing profile do not rewrite a document already issued. |
| payment | { method, reference, paidAt }. reference is the payment provider’s own id, for reconciling against a bank statement. |
| Status | When |
|---|---|
| 200 | The invoice, or the list. |
| 403 forbidden | The credential holds neither account:read nor billing:read. |
| 404 not_found | No such invoice, an invoice belonging to another account, or an id that is not an inv_ id. |
Invoices are also emailed, from [email protected] — which is a real mailbox that accepts replies. If the mail never arrives the invoice still exists, is still numbered, and is still readable here: the document does not depend on the message.
NextErrors →Every error type, and how to handle each one.