Topograph can notify you about three billing events:
- Low balance: your prepaid credit balance crosses a configured threshold.
- High usage: your spend in a rolling window reaches a configured threshold. High usage has two independent passes, described below.
- Auto top-up: an automatic recharge succeeds or fails.
Each notification can go out as an email (to your account admins) and as a webhook event (delivered through the same Svix infrastructure as the company data webhooks). You can independently enable or disable each channel per notification kind.
All master toggles are off by default on new accounts. Nothing fires until an admin enables the specific kind they want from the Billing page. Channel sub-toggles (email / webhook) start at on, so flipping a master routes through both channels with a single click.
How notifications fire
Notifications are evaluated after every billable reserve. The moment a request debits credits from your account, the billing layer checks whether any threshold was just crossed and records an audit row in billing_notification_event. A row is only recorded once per crossing: if you drop from €100 to €40 and the warning threshold is €50, we fire one warning row, not one per request that ticked the balance down. The tier is rearmed when you refill above the threshold.
Global vs per-workspace high usage
High usage runs two independent passes on every reserve. Both can fire in the same evaluation, and overriding one does not affect the other:
- Global (account aggregate): sums your billable events across every workspace over the last
globalHighUsagePeriodMinutes and checks it against globalHighUsageTiers. Fires once per account with workspaceId: null on the event row. Use this to alert on total account burn regardless of which workspace drove it.
- Per-workspace: sums billable events for the triggering workspace over the last
highUsagePeriodMinutes and checks it against highUsageTiers. Fires once per workspace with workspaceId set on the event row. The values you set on the config act as the default for every workspace; specific workspaces can override them via the workspace config endpoint.
Each pass has its own master toggle, channels, period, and tier list. High usage uses formal absolute thresholds. There is no multiplier or baseline comparison.
Auto top-up notifications fire at the moment the auto top-up workflow succeeds or fails, using the payment intent ID (or the workflow run ID on early errors) as the dedup key.
Channels
Every notification kind has two channels, configured independently:
- Email: sent via Resend to the admins of your Clerk organization (with a fallback to any user on the account if Clerk is unavailable).
- Webhook: delivered through Svix using your existing account
svixApplicationId. Received through the same endpoints you already set up for company.updated.
You control each channel with a boolean. A notification is sent on a given channel when the kind’s master switch AND the channel switch are both on. Turning both channel switches off but leaving the master on is a valid “audit-only” mode: the row lands in billing_notification_event but nothing is dispatched.
Webhook event types
All billing webhook payloads carry a type and version: "1". Future schema changes will emit new event types rather than mutate v1.
billing.low_balance.triggered
{
"type": "billing.low_balance.triggered",
"version": "1",
"accountId": "acc_...",
"tier": "warning",
"balanceCents": 4523,
"thresholdCents": 5000,
"autoTopupEnabled": true,
"firedAt": "2026-04-14T10:23:45.000Z"
}
The tier field is one of warning, critical, or depleted. You can configure up to 10 tiers via the REST API. The starter default is a single warning tier at €1000, a buffer that comfortably covers the 5 to 7 business day SEPA and wire settlement window.
billing.high_usage.triggered
The high-usage event now carries a scope field that tells you which pass fired:
scope: "workspace" with a set workspaceId: the per-workspace pass fired for that workspace.
scope: "global" with workspaceId: null: the global aggregate pass fired for the whole account.
Per-workspace example:
{
"type": "billing.high_usage.triggered",
"version": "1",
"accountId": "acc_...",
"scope": "workspace",
"workspaceId": "ws_...",
"tier": "warning",
"periodMinutes": 60,
"periodSpendCents": 2300,
"thresholdCents": 2000,
"balanceCents": 87000,
"firedAt": "2026-04-14T10:23:45.000Z"
}
Global aggregate example:
{
"type": "billing.high_usage.triggered",
"version": "1",
"accountId": "acc_...",
"scope": "global",
"workspaceId": null,
"tier": "critical",
"periodMinutes": 60,
"periodSpendCents": 61000,
"thresholdCents": 50000,
"balanceCents": 200000,
"firedAt": "2026-04-14T10:23:45.000Z"
}
Each pass ships with a single warning tier. Both global and per-workspace high usage start at €1000 over a 1-day rolling window. Add more tiers from the UI or via PATCH.
Workspace overrides only affect the per-workspace pass. Setting highUsageEnabled: false on a workspace override stops that workspace from firing its own per-workspace notifications but does not silence the global pass. If you want to silence the global pass too, turn off globalHighUsageEnabled at the account level.
billing.auto_topup.succeeded
{
"type": "billing.auto_topup.succeeded",
"version": "1",
"accountId": "acc_...",
"amountCents": 10000,
"previousBalanceCents": 350,
"newBalanceCents": 10350,
"thresholdCents": 5000,
"paymentIntentId": "pi_...",
"firedAt": "2026-04-14T10:23:45.000Z"
}
billing.auto_topup.failed
{
"type": "billing.auto_topup.failed",
"version": "1",
"accountId": "acc_...",
"attemptedAmountCents": 10000,
"currentBalanceCents": 350,
"errorMessage": "card_declined",
"paymentIntentId": null,
"autoTopupDisabled": true,
"firedAt": "2026-04-14T10:23:45.000Z"
}
When the auto top-up attempt fails, the workflow disables auto top-up on the account to prevent retry loops. You will receive this event once and then need to update your payment method to re-enable it.
Configuration API
All endpoints are under /v2/billing/notifications and require your standard x-api-key header.
Read the resolved config
GET /v2/billing/notifications/config
Returns the fully merged config: your stored overrides layered on top of the defaults. You always receive a complete shape, even if you have not configured anything yet.
A brand-new account that has never touched the config sees every master at false and every channel at true. Each kind ships with a single warning tier so flipping the master on immediately fires on one sensible threshold. Add more tiers (critical, depleted, extra bands) via PATCH if you want finer granularity.
{
"lowBalanceEnabled": false,
"lowBalanceEmailEnabled": true,
"lowBalanceWebhookEnabled": true,
"lowBalanceTiers": [
{ "tier": "warning", "cents": 100000 }
],
"globalHighUsageEnabled": false,
"globalHighUsageEmailEnabled": true,
"globalHighUsageWebhookEnabled": true,
"globalHighUsagePeriodMinutes": 1440,
"globalHighUsageTiers": [
{ "tier": "warning", "cents": 100000 }
],
"highUsageEnabled": false,
"highUsageEmailEnabled": true,
"highUsageWebhookEnabled": true,
"highUsagePeriodMinutes": 1440,
"highUsageTiers": [
{ "tier": "warning", "cents": 100000 }
],
"autoTopupNotificationsEnabled": false,
"autoTopupEmailEnabled": true,
"autoTopupWebhookEnabled": true
}
Update the config
PATCH /v2/billing/notifications/config
Content-Type: application/json
{
"lowBalanceTiers": [
{ "tier": "warning", "cents": 10000 },
{ "tier": "critical", "cents": 2000 },
{ "tier": "depleted", "cents": 0 }
],
"highUsagePeriodMinutes": 120,
"highUsageTiers": [
{ "tier": "critical", "cents": 50000 },
{ "tier": "warning", "cents": 10000 }
],
"lowBalanceEmailEnabled": false
}
All fields are optional. Any field you omit keeps its persisted value. You can send just {"lowBalanceEmailEnabled": false} to turn off low-balance emails without touching anything else.
List recent notification events
GET /v2/billing/notifications/recent?limit=50
Returns the most recent billing_notification_event rows for your account, ordered newest first. Each row includes the kind, identifier (tier name for threshold events, succeeded or failed for auto-topup), the original payload, and two flags: emailSent and webhookSent. The flags are flipped by the dispatch path after each channel successfully delivers.
Workspace overrides
If you use workspaces, you can override the high-usage rules for a specific workspace. Low-balance and auto-topup stay at the account level.
GET /v2/billing/notifications/workspaces/{workspaceId}/config
PATCH /v2/billing/notifications/workspaces/{workspaceId}/config
DELETE /v2/billing/notifications/workspaces/{workspaceId}/config
GET returns both the resolved account config and the raw workspace override row, so you can see which fields are overridden and which are inherited. PATCH upserts an override; any null field falls back to the account config at evaluation time. DELETE removes the override row (the workspace then inherits everything).
Example: give workspace ws_batch a much higher high-usage floor so nightly backfills don’t page you, while keeping everything else at the account defaults:
PATCH /v2/billing/notifications/workspaces/ws_batch/config
Content-Type: application/json
{
"highUsageTiers": [
{ "tier": "critical", "cents": 500000 },
{ "tier": "warning", "cents": 100000 }
]
}
Verifying webhooks
Billing webhooks use the same Svix application as your company.updated webhooks. The signature verification flow is identical:
import { Webhook } from 'svix';
const wh = new Webhook(process.env.SVIX_SIGNING_SECRET);
const evt = wh.verify(req.rawBody, req.headers);
if (evt.type === 'billing.low_balance.triggered') {
// handle low-balance alert
}
See the Webhooks guide for the full verification walkthrough and retry semantics.
Idempotency
Every billing notification is assigned a stable dedupKey at evaluation time. If the same crossing is re-evaluated (for example, because a Temporal activity retried), the second attempt is caught by a unique constraint on dedupKey and no second audit row is written.
Dedup key shapes:
- Low balance:
${accountId}:low_balance:${tier}
- Global high usage:
${accountId}:global:high_usage:${tier}:${periodBucketIso}
- Per-workspace high usage:
${accountId}:${workspaceId}:high_usage:${tier}:${periodBucketIso}
- Auto top-up:
${accountId}:auto_topup:{succeeded or failed}:${paymentIntentId or workflowRunId}
The period bucket for high-usage is the ISO timestamp of the current period boundary (floor(now / periodMs) * periodMs), so two fires inside the same period collapse into one dedup key. The global pass uses globalHighUsagePeriodMinutes for its bucket; the per-workspace pass uses the effective highUsagePeriodMinutes (after any workspace override).
Rearm semantics
Threshold notifications follow a Schmitt-trigger pattern. A tier fires once when you cross it, then disarms. It rearms when the condition has clearly reversed:
- Low balance: when the balance recovers strictly above the tier’s
cents value.
- High usage: when the period spend falls strictly below the tier’s
cents value.
This means a single drop to €0 will not spam you with repeated depleted notifications, but if you refill to €100 and then drain back to €0, you get a fresh notification.
Auto-topup notifications are event-based, not threshold-based. Each attempt is its own row; there is no state to rearm.