Get an API key

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

GET/v1/billingaccount:read
curl 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
# }
FieldNotes
planThe plan in force: { id, name, priceUsd }. priceUsd is the one published price and is identical for everyone.
subscriptionnull 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.
profileThe billing identity: legal name, billing email, country, tax id. null until it has been filled in.
paymentsThe twenty most recent, newest first, with status, amountMinor, taxAmountMinor, method, paidAt and failureReason.
plansEvery 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.
configuredWhether a payment provider is configured on this deployment at all.

Money is always in minor units900 with "currency": "USD" is $9.00. Integers, never floats, so nothing rounds on the way through JSON.

The commercial history

GET/v1/billing/historyaccount:read

An 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…" }
#   ]
# }
FieldNotes
chain{ valid, brokenAt, reason } — the whole ledger replayed and verified on this read.
data[].typeOne of plan_changed, subscription_created, subscription_activated, subscription_past_due, subscription_cancelled, payment_succeeded, payment_failed.
data[].actorWho caused it — a user, the payment provider, or the system.
data[].reasonFree text where one was recorded, otherwise null.
data[].detailThe event’s own payload. Shape varies by type.
data[].hashThe chain link. Each entry commits to the one before it.

Invoices

GET/v1/billing/invoicesaccount:read
GET/v1/billing/invoices/:idaccount:read

An 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.

FieldNotes
idThe opaque handle, inv_ prefixed. Use this in the URL.
numberThe 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.
issuedAtWhen the document was raised.
currencyISO 4217. All amounts below are minor units of it.
subtotalMinor / taxMinor / totalMinorBase, tax and the sum. taxMinor is 0 where no tax was charged.
taxRateBpThe rate applied, in basis points (1800 = 18%), or null when no tax applied.
paidMinorWhat was actually received against it.
plan / periodStart / periodEndWhat 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.
StatusWhen
200The invoice, or the list.
403 forbiddenThe credential holds neither account:read nor billing:read.
404 not_foundNo 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.

NextErrorsEvery error type, and how to handle each one.