Checking your session…

Billing & Credits

Billing on the platform comes down to one model. Hold onto it and every endpoint in this chapter is just a window onto it:

Your account has a single pool of credits. 1 credit = 1 second of assistant time. Your plan adds credits to the pool every month. Everything your assistants do — calls, chats, automations — draws from that pool. You can optionally ring-fence part of the pool for one app (a cap), so that app can never spend more than you've set aside. Auto top-up keeps the pool from running dry. The ledger shows every movement, in and out.

That's the whole system. In detail:

  • The pool. One balance per account, shared by all your apps. Buy credits or receive your monthly plan allowance and they land here; usage drains from here.
  • Caps (optional). Give one app its own ring-fenced allowance, moved out of the pool. A capped app spends only its cap — useful for bounding spend per app or per customer. Uncapped apps draw straight from the pool.
  • Auto top-up (optional). When a balance falls below your threshold, the platform charges your saved card automatically, within limits you set.
  • The ledger. Every debit and credit — each call, each top-up, each cap movement — is one row, queryable per app and per account.

A small number of apps are billed individually rather than from a pool (billingMode: "app"); for those, read "the app's own balance" wherever this chapter says "the pool".

Requires view_billing. Returns your account's pool snapshot (balance, ownerEmail, billingMode) plus a per-app summary. Apps are scoped to your account — you never see another account's apps here.

Requires view_billing. Returns every visible app with its billing snapshot (balance, tier, cap state, ownerEmail) — the data behind the dashboard's apps grid.

GET/api/v1/billing/merchantview_billing
curl 'https://api.everycallhandled.com/api/v1/billing/merchant' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.
GET/api/v1/billing/botsview_billing
curl 'https://api.everycallhandled.com/api/v1/billing/bots' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.

Requires view_billing.

Query params: month=YYYY-MM OR from/to, category, limit (default 50, max 200)

Response (200):

The summary form returns { "totals": { byCategory, byDirection }, "balanceStart", "balanceEnd", "rowCount" }.

GET/api/v1/apps/{appId}/ledger?month=2026-05&limit=50view_billing
GET/api/v1/apps/{appId}/ledger/summary?month=2026-05view_billing
curl 'https://api.everycallhandled.com/api/v1/apps/{appId}/ledger' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.
curl 'https://api.everycallhandled.com/api/v1/apps/{appId}/ledger/summary' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.
JSON
{
  "items": [
    {
      "id": "...",
      "occurredAt": "2026-05-10T11:32:30Z",
      "category": "voice-phone",
      "direction": "debit",
      "amountCredits": 145,
      "balanceAfter": 13157,
      "note": "Call from +44...",
      "ref": "call-_+447941190142_abc123",
      "billingSource": "merchant"
    }
  ],
  "nextCursor": "..."
}

Requires view_billing. Same shape as the app variants, aggregated across all apps under an account. The caller's account must match or be a parent of the queried account. Use the summary form for dashboards showing customer-by-customer usage; use the full form for line-by-line investigation.

GET/api/v1/merchants/{merchantId}/ledgerview_billing
GET/api/v1/merchants/{merchantId}/ledger/summaryview_billing
curl 'https://api.everycallhandled.com/api/v1/merchants/{merchantId}/ledger' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.
curl 'https://api.everycallhandled.com/api/v1/merchants/{merchantId}/ledger/summary' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.

Your plan sets your monthly credit allowance and your per-minute rate. Plan management is fully self-serve.

Requires view_billing.

Response (200):

Accounts without a self-serve subscription get { "eligible": false, "plans": [], "current": null } — plan changes for those accounts go through support. Accounts on a legacy (grandfathered) price get legacyPlan: true and an empty plans array: switching would irreversibly forfeit the legacy deal, so no targets are offered.

GET/api/v1/dashboard/billing/plansview_billing
curl 'https://api.everycallhandled.com/api/v1/dashboard/billing/plans' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.
JSON
{
  "eligible": true,
  "currency": "gbp",
  "legacyPlan": false,
  "current": { "priceId": "price_...", "tier": "pro", "interval": "month", "label": "Pro (monthly)", "amountPence": 19900 },
  "cancelAtPeriodEnd": false,
  "cancelAt": null,
  "currentPeriodEnd": 1753968000,
  "plans": [
    { "priceId": "price_...", "tier": "starter", "isCurrent": false, "direction": "downgrade" },
    { "priceId": "price_...", "tier": "pro", "isCurrent": true, "direction": "same" }
  ]
}

Requires edit_billing (the one grantable "manage billing" permission covers all three).

Change plan — body { "tier": "pro" } (or "priceId", optionally "interval": "month"). The new price is applied with proration; the platform then rewrites the account tier and grants any upgrade credits shortly after — the response confirms the switch was submitted, not that credits have landed yet.

Response (200): { "changed": true, "targetPriceId": "price_...", "proration_behavior": "create_prorations", "direction": "upgrade" } — or { "changed": false, "reason": "already-on-plan" }.

Errors: 422 no active subscription; 400 unknown/non-switchable plan; 409 legacy-plan when the current price is grandfathered — resend with "confirmForfeitLegacy": true only after the account holder explicitly accepts losing the legacy deal (it cannot be restored).

Cancel — end-of-period cancel with an empty body: renewals stop but everything keeps working until the period ends, and prepaid credits are kept (no clawback).

Response (200): { "scheduledCancel": true, "cancelAt": 1753968000, "currentPeriodEnd": 1753968000 }

Resume — undoes a scheduled cancel any time before the period end. Empty body.

Response (200): { "resumed": true, "currentPeriodEnd": 1753968000 }

POST/api/v1/dashboard/billing/change-planedit_billing
POST/api/v1/dashboard/billing/canceledit_billing
POST/api/v1/dashboard/billing/resumeedit_billing
curl -X POST 'https://api.everycallhandled.com/api/v1/dashboard/billing/change-plan' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"tier":"pro"}'
Sign in to run this against your account.
curl -X POST 'https://api.everycallhandled.com/api/v1/dashboard/billing/cancel' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.
curl -X POST 'https://api.everycallhandled.com/api/v1/dashboard/billing/resume' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.

For apps funded by the pool (billingMode="merchant"), you can ring-fence part of the pool as a per-app cap. A capped app spends only its cap; the pool only moves when you allocate more credits or when the monthly refill runs. Use caps to bound spend per app — or, as a partner, per customer.

How cap state reads:

  • GET /api/v1/apps/{appId} returns the cap attributes inline: merchantCapCredits (current cap balance), merchantCapAllocated (lifetime allocated), merchantCapMonthlyAllowance, merchantCapCarryForward, merchantCapLastReset.
  • If merchantCapAllocated > 0, the app is capped — usage drains the cap, not the pool.
  • merchantCapMonthlyAllowance > 0 turns on automatic monthly refill (cycle anchored to first allocation).

Monthly refill semantics. The refill runs on each app's monthly anchor date. Important — expired credits evaporate; they do not return to the pool:

Behavioural notes:

  • Unused allowance carries forward exactly one month. Anything still unused a month later evaporates. There is no second grace period.
  • The pool funds the full allowance on every refill — including when expired credits would have "covered" the cost. If you build reconciliation logic that assumes expired credits return to the pool, your numbers will diverge from ours.
  • Pool-insufficient blocks the entire refill. If the pool can't cover the allowance, the cap stays as-is for that cycle (no partial refill) and the account is alerted.
  • Top-up bundles bought separately stay on the pool until either consumed by direct usage or pulled into a per-app cap by the next refill.
text
Let A = merchantCapMonthlyAllowance
    B = current merchantCapCredits (cap balance)
    C = current merchantCapCarryForward (last cycle's leftover)
    P = current account pool balance

On refill:
    expiring = min(C, B)        // last cycle's leftover, still unspent → evaporates
    newCap   = (B - expiring) + A
    newCarry = B - expiring     // this cycle's contribution becomes next cycle's carry-forward
    poolDelta = -A              // pool ALWAYS funds A; expired credits VANISH (not returned)

If P < A:
    block + alert; no write

Requires edit_billing.

Body:

  • credits (int, required) — credits to move from pool to app cap (1 credit = 1 second).
  • idempotencyKey (string, recommended) — UUID to safely retry. Without it, an accidental double-click double-allocates.
  • setLastReset (bool, default false) — when toggling merchantCapMonthlyAllowance on for the first time, set true so the scheduler doesn't immediately refill again.

Response (200):

Errors:

  • 400 INSUFFICIENT_POOL_BALANCE — the pool can't cover the request.
POST/api/v1/apps/{appId}/cap-topupedit_billing
curl -X POST https://api.everycallhandled.com/api/v1/apps/$APP_ID/cap-topup \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "credits": 6000,
    "idempotencyKey": "'$(uuidgen)'",
    "setLastReset": false
  }'
Sign in to run this against your account.
JSON
{
  "success": true,
  "appId": "...",
  "creditsAllocated": 6000,
  "capBalance": 9600,
  "capAllocatedTotal": 24000,
  "poolBalance": 53000
}

The inverse of cap top-up: returns the app's remaining cap balance to the pool and zeroes every cap attribute (balance, lifetime allocated, monthly allowance, carry-forward, last reset), so the app goes back to uncapped and draws directly from the pool again.

Requires edit_billing. Only valid for apps with billingMode=merchant.

Body:

  • idempotencyKey (string, recommended) — UUID to safely retry. A replayed request returns the same result with idempotent: true instead of moving credits twice.

Response (200):

If the app is already uncapped this is an honest no-op: { "removed": false, "reason": "..." } with a 200.

Notes:

  • The transfer is ordered pool-first (credit the pool, then zero the cap), so a mid-flight failure can only ever leave extra credits, never lost ones, and is rolled back automatically.
  • Two ledger rows record the move (cap-remove-debit on the app, cap-remove-credit on the pool) — you'll see them in the ledger endpoints above.
  • If the app had a per-app auto top-up configured while capped, removing the cap means future auto top-up reads follow the pool again (see below).
POST/api/v1/apps/{appId}/cap-removeedit_billing
curl -X POST https://api.everycallhandled.com/api/v1/apps/$APP_ID/cap-remove \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "idempotencyKey": "'$(uuidgen)'" }'
Sign in to run this against your account.
JSON
{
  "removed": true,
  "appId": "...",
  "returnedToPool": 3600,
  "poolBalance": 56600,
  "merchantId": "acme"
}

Auto top-up charges the saved card automatically when a credit balance falls below a threshold, so a busy line never goes dark mid-month. Nothing is charged when you call these endpoints — they only read and write the configuration; the platform performs the actual off-session charge when the threshold is crossed.

There are two scopes, matching the pooled billing model:

  • Account (pool) level — the natural home for most customers: one setting that keeps the shared pool topped up, funding every uncapped app.
  • App level — for individually billed apps and apps with a per-app cap, where the app has its own balance to protect.

Both scopes share the same configuration shape:

  • enabled (bool) — master switch.
  • thresholdSeconds (int) — top up when the balance drops below this many credit-seconds. Maximum 36,000 (600 minutes) for customer saves.
  • amountPence (int) — how much to charge per top-up: minimum 500 (£5), maximum 100,000 (£1,000).
  • monthlyCapPence (int) — spending ceiling per calendar month; 0 means no cap. If set, it must be at least one top-up amount.

Reads also return hasSavedCard (whether a card is on file from a plan purchase) and source ("merchant" when the config lives on the pool, "app" when it lives on the app).

Safety rails on execution, regardless of configuration: at least 1 hour between charges, at most 3 charges per day, and the monthly cap is always honoured. Credits are purchased at your plan's per-minute rate (the same rate as a manual top-up).

JSON
{
  "enabled": true,
  "thresholdSeconds": 600,
  "amountPence": 2000,
  "monthlyCapPence": 10000
}

Requires view_billing. You can read your own account's pool setting; a parent account can read a child's.

GET/api/v1/accounts/{merchantId}/auto-topupview_billing
GET/api/v1/apps/{appId}/auto-topupview_billing
curl 'https://api.everycallhandled.com/api/v1/accounts/{merchantId}/auto-topup' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.
curl 'https://api.everycallhandled.com/api/v1/apps/{appId}/auto-topup' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.

Requires edit_billing.

The app-level endpoint is scope-aware: it follows the app's billing source. For an individually billed app or a capped app, the config protects the app's own balance. For an uncapped pool-funded app, reads and writes fall through to the shared pool config — the same setting the account-level endpoint manages — and the response says so with "source": "merchant". If two uncapped apps both "have" auto top-up configured, it's one pool setting seen from two places, not two settings.

Errors:

  • 409 — enabling without a saved card. Purchase a plan first (that saves the card), then enable.
  • 403 — the account is outside your scope.
PATCH/api/v1/accounts/{merchantId}/auto-topupedit_billing
PATCH/api/v1/apps/{appId}/auto-topupedit_billing
curl -X PATCH 'https://api.everycallhandled.com/api/v1/apps/{appId}/auto-topup' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"enabled":true,"thresholdSeconds":1200,"amountPence":2000,"monthlyCapPence":10000}'
Sign in to run this against your account.
curl -X PATCH https://api.everycallhandled.com/api/v1/accounts/$ACCOUNT_ID/auto-topup \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "enabled": true, "thresholdSeconds": 1200, "amountPence": 2000, "monthlyCapPence": 10000 }'
Sign in to run this against your account.