Skip to content
Contents
Usage and account

Billing

What this workspace is subscribed to, what the current period has accrued, and the statements behind it. It is account-management surface: no API key reaches any of it.

What is here

One overview, one profile write, and the statement history. Everything is read with the workspace's own credentials — there is no cross-tenant route here.

GET/v1/billingPATCH/v1/billing/customerGET/v1/billing/invoicesGET/v1/billing/invoices/{invoiceId}

Session credentials only

All four routes are declared for sessionToken alone and require billing.manage. An API key cannot reach them at all, so hide the destination for a role without the permission rather than letting a request 403 — and re-check on the panel itself, because a bookmarked URL deserves an explanation rather than a failed request. See authentication.

curl
# No API key reaches this: billing is account-management surface.
curl https://whats.azzamkh.sa/api/v1/billing \
  -H "Authorization: Bearer <access token>" \
  -H "X-Workspace-Id: ws_..."

# 200 OK — every amount is a MINOR-UNIT integer beside its own currency,
# and null means NOT PRICED, which is a different fact from 0.
# {
#   "customer": {
#     "id": "bcu_...", "provider": "manual", "name": "...",
#     "email": "...", "currency": null, ...
#   },
#   "subscription": {
#     "status": "active",
#     "plan": { "code": "standard", "name": "Standard", ... },
#     "pending_plan": null,
#     "current_period_start": "2026-08-01T00:00:00.000Z",
#     "current_period_end": "2026-09-01T00:00:00.000Z",
#     "canceled_at": null,
#     "items": [ ... ]
#   },
#   "latest_invoice": {
#     "id": "inv_...", "number": null, "status": "open",
#     "currency": null, "subtotal_minor": null, "total_minor": null,
#     "unpriced_lines": 1, "period_end": "2026-09-01T00:00:00.000Z", ...
#   }
# }

Reading the money

  • Every amount is a MINOR-UNIT INTEGER. There is no exponent to assume: JOD has three decimal places and JPY has none, so dividing by 100 anywhere is wrong for both.
  • Each amount carries its OWN currency. There is no deployment currency and no default — read the one beside the figure.
  • null means NOT PRICED, which is a different fact from 0. A free plan legitimately costs zero; an unpriced line has never been given an amount.
  • Nothing is re-derived on the client. Not the period — the server anchors both boundaries on UTC midnights and clamps the day of month so a renewal cannot drift — not a line's amount, and not the total.

A total is only as complete as unpriced_lines says

total_minor sums the PRICED lines, and unpriced_lines counts the ones it left out. Render the count beside every total, or a customer reads the subscription fee as the whole bill. It is ordinarily at least 1 today: every subscription carries a metered line and no route prices usage yet, which is why this surface reads as a usage STATEMENT rather than as a bill.

The responses

Subscription

FieldDescription
statusThe subscription's own status.
planThe plan in force right now.
pending_planA decided-but-unapplied plan change. See below.
current_period_start, current_period_endThe current period's boundaries. period_end is EXCLUSIVE — do not shift it back a day to label it.
canceled_atWhen it was cancelled, if it was.
itemsThe subscription lines, each with its own amount and currency.

pending_plan is not a preference

It is a plan change parked to apply at the end of the period. The customer cannot cancel it and may not have asked for it, so it belongs above the screen rather than inside a card. effective_at is usually null — a change parked at period end has no instant of its own — so name the period end rather than printing an empty date.

Invoice

FieldTypeDescription
numberstring | nullMinted at ISSUE, and deliberately not gap-free. An invoice here is not a tax document.
statusstringIts position in the invoice lifecycle.
period_start, period_endstringThe period it covers. period_end is exclusive.
subtotal_minor, total_minornumber | nullBoth null when nothing on the invoice is priced.
unpriced_linesnumberHow many lines the total left out. Always render it beside a total.
issued_at, due_at, paid_at, voided_atstring | nullWhen it was issued, is due, was paid, was voided.
linesarrayPresent on the detail route; absent from the list.

An invoice is FROZEN when its period closes: it keeps saying the same thing after a plan is edited or a price changes. Each line carries quantity, an optional metric, and unit_amount_minor and amount_minor that are null for anything unpriced.

Billing profile

PATCH /v1/billing/customer takes the name, email, tax id, country, currency and address. Every field is nullable, and sending null clears it. provider is read-only.

Changing plan

There is no self-service plan change, and that is deliberate: with the manual provider, collection happens outside this system, so an upgrade button would commit a customer to something the platform cannot complete. The plan catalogue is what a plan comparison is built from, and plans and quotas covers what the current plan actually allows.

Errors that matter here

CodeHTTPWhen it happens
BILLING_SUBSCRIPTION_NOT_FOUND404This workspace has no active subscription.
BILLING_INVOICE_NOT_FOUND404No invoice with that id in this workspace.
BILLING_INVOICE_INVALID_STATE409The invoice cannot move to that status from its current one. details.allowed carries the transitions that ARE permitted, and it is the authority — no endpoint publishes the table up front, so render what the refusal says.
BILLING_PROVIDER_UNAVAILABLE503The billing provider is unavailable. Retryable.
FORBIDDEN403The caller does not hold billing.manage.

Every code and its status is on the errors page.