# Balance and compliance

> Every field of GET /v1/balance, GET /v1/usage (and its events and CSV export), GET /v1/compliance and POST /v1/compliance/accept.

<!-- Generated by apps/docs/scripts/gen-reference.mjs from apps/docs/public/openapi/v1.json. Do not edit: change the API's schemas (then `pnpm --filter @seasalt/api openapi:emit`) or the notes in apps/docs/src/reference-notes/, then run `pnpm --filter @seasalt/docs gen:reference`. -->

The account facts a request can be refused over: the credit balance (a send or a campaign start) and the outbound compliance acknowledgment (a campaign start only); and what the credits went on (usage). How-to: [Credits and balance](/platform-api/billing/#credits-and-balance), [Usage and spend](/platform-api/billing/#usage-and-spend), [Accept the outbound compliance acknowledgment](/platform-api/campaigns/#accept-the-outbound-compliance-acknowledgment).

Generated from [`/openapi/v1.json`](/openapi/v1.json); interactive version: [API reference](/openapi/v1/).

## Endpoints

| method | path | what it does | success |
|---|---|---|---|
| [`GET`](#get-v1balance) | `/v1/balance` | Read the credit balance | `200` |
| [`GET`](#get-v1usage) | `/v1/usage` | Report what the credits went on | `200` |
| [`GET`](#get-v1usageevents) | `/v1/usage/events` | List charges one by one, newest first | `200` |
| [`GET`](#get-v1usageexport) | `/v1/usage/export` | Download the charges as CSV | `200` |
| [`GET`](#get-v1compliance) | `/v1/compliance` | Read the outbound compliance acknowledgment | `200` |
| [`POST`](#post-v1complianceaccept) | `/v1/compliance/accept` | Accept the outbound compliance acknowledgment | `200` |

## `GET /v1/balance`

Read the credit balance.

The workspace's credits, what can be spent now (`available_credits`) and whether it is suspended: what `402 insufficient_credits`, `402 billing_not_eligible` and `402 workspace_suspended` are decided on. Any key of the workspace. The balance is at most about 5 minutes old (`updated_at`), so reading it more often than that does not make it fresher. Add credits in the dashboard under Settings, Billing.

**Answers** `200` with [Balance](#balance). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [balance and compliance](/platform-api/errors/#balance-and-compliance), [every request](/platform-api/errors/#every-request).

## `GET /v1/usage`

Report what the credits went on.

Credits charged in a window, in total and grouped by `category` (default), UTC `day` and/or `label` (what was charged, in words). Default window: the current billing cycle (`cycle_start`) to now, the same usage `pending_credits` on `GET /v1/balance` counts. `from` is inclusive, `to` exclusive, at most 400 days apart. Any key of the workspace. Not paged; a day with no usage has no group.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `from` | query | string | Start of the window, inclusive: an ISO 8601 date (midnight UTC) or instant. Default: the start of the current billing cycle (`cycle_start`). |
| `to` | query | string | End of the window, exclusive. Default: now. At most 400 days after `from`. To make `/v1/usage`, `/v1/usage/events` and `/v1/usage/export` answer the same rows, pass all three the `from` and `to` that `/v1/usage` echoed. |
| `category` | query | string<br>`phone`, `recording`, `speech`, `ai`, `messaging`, `whatsapp`, `email`, `numbers`, `other` | Only this category: `phone` (call minutes, attempts, voicemails and short calls), `recording` (call connection and call recording minutes, recording storage), `speech` (speech-to-text, text-to-speech and realtime voice), `ai` (AI model tokens and embeddings), `messaging` (SMS and MMS, and messages on chat channels (Telegram, Messenger, Instagram, LINE, Slack, Discord)), `whatsapp` (WhatsApp conversation and platform fees), `email` (email sent), `numbers` (phone number rental), `other` (anything else (knowledge-base crawls, workflow steps, new kinds of usage)). |
| `group_by` | query | string | One or two of `day`, `category` and `label`, comma-separated (default `category`). `units` and `unit` are filled only with `label`. |

</div>

**Answers** `200` with [UsageReport](#usagereport). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [balance and compliance](/platform-api/errors/#balance-and-compliance), [every request](/platform-api/errors/#every-request).

## `GET /v1/usage/events`

List charges one by one, newest first.

Every charge in the window, newest first, with its category, label, credits and the conversation, agent and campaign it was for. Same `from`, `to` and `category` as `GET /v1/usage`: pass the `from` and `to` it echoed to read the same rows, and pass both to walk a fixed set (a default window is resolved again on every page). Any key of the workspace.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `from` | query | string | Start of the window, inclusive: an ISO 8601 date (midnight UTC) or instant. Default: the start of the current billing cycle (`cycle_start`). |
| `to` | query | string | End of the window, exclusive. Default: now. At most 400 days after `from`. To make `/v1/usage`, `/v1/usage/events` and `/v1/usage/export` answer the same rows, pass all three the `from` and `to` that `/v1/usage` echoed. |
| `category` | query | string<br>`phone`, `recording`, `speech`, `ai`, `messaging`, `whatsapp`, `email`, `numbers`, `other` | Only this category: `phone` (call minutes, attempts, voicemails and short calls), `recording` (call connection and call recording minutes, recording storage), `speech` (speech-to-text, text-to-speech and realtime voice), `ai` (AI model tokens and embeddings), `messaging` (SMS and MMS, and messages on chat channels (Telegram, Messenger, Instagram, LINE, Slack, Discord)), `whatsapp` (WhatsApp conversation and platform fees), `email` (email sent), `numbers` (phone number rental), `other` (anything else (knowledge-base crawls, workflow steps, new kinds of usage)). |
| `limit` | query | integer<br>1–200 | Page size, 1 to 200 (default 50). |
| `after` | query | string | The previous page's `next_cursor`. |

</div>

**Answers** `200` with [UsageEventList](#usageeventlist). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [balance and compliance](/platform-api/errors/#balance-and-compliance), [every request](/platform-api/errors/#every-request).

## `GET /v1/usage/export`

Download the charges as CSV.

The rows of `GET /v1/usage/events` for the same `from`, `to` and `category`, as one CSV file (`recorded_at,category,label,units,unit,credits,conversation_id,agent_id,campaign_id`), newest first. At most 250,000 rows: over that the export is refused before any byte (`400 export_too_large`), never cut short in the file. One export at a time per workspace. If the connection is cut mid-file, the body is incomplete: compare the row count to `events` from `GET /v1/usage`. Text cells that start with `=`, `+`, `-` or `@` get a leading `'`. Any key of the workspace.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `from` | query | string | Start of the window, inclusive: an ISO 8601 date (midnight UTC) or instant. Default: the start of the current billing cycle (`cycle_start`). |
| `to` | query | string | End of the window, exclusive. Default: now. At most 400 days after `from`. To make `/v1/usage`, `/v1/usage/events` and `/v1/usage/export` answer the same rows, pass all three the `from` and `to` that `/v1/usage` echoed. |
| `category` | query | string<br>`phone`, `recording`, `speech`, `ai`, `messaging`, `whatsapp`, `email`, `numbers`, `other` | Only this category: `phone` (call minutes, attempts, voicemails and short calls), `recording` (call connection and call recording minutes, recording storage), `speech` (speech-to-text, text-to-speech and realtime voice), `ai` (AI model tokens and embeddings), `messaging` (SMS and MMS, and messages on chat channels (Telegram, Messenger, Instagram, LINE, Slack, Discord)), `whatsapp` (WhatsApp conversation and platform fees), `email` (email sent), `numbers` (phone number rental), `other` (anything else (knowledge-base crawls, workflow steps, new kinds of usage)). |

</div>

**Answers** `200` (`text/csv`) with the file. Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [balance and compliance](/platform-api/errors/#balance-and-compliance), [every request](/platform-api/errors/#every-request).

## `GET /v1/compliance`

Read the outbound compliance acknowledgment.

Whether the workspace accepted the current outbound compliance acknowledgment, its text and its `current_version`. A workspace's first campaign start needs it (`409 outbound_compliance_required`). Any key of the workspace.

**Answers** `200` with [OutboundCompliance](#outboundcompliance). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [balance and compliance](/platform-api/errors/#balance-and-compliance), [every request](/platform-api/errors/#every-request).

## `POST /v1/compliance/accept`

Accept the outbound compliance acknowledgment.

Accepts the text `GET /v1/compliance` returned, for the whole workspace, as the key's owner. Owners and admins only. Send the `current_version` you read, so you accept the text you were shown. Accepting again changes nothing: the first acceptance (who and when) is kept.

**Request body:** [ComplianceAccept](#complianceaccept).

**Answers** `200` with [OutboundCompliance](#outboundcompliance). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [balance and compliance](/platform-api/errors/#balance-and-compliance), [every request](/platform-api/errors/#every-request).

## Objects

### Balance

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `balance` |   |
| `credits` | number, required | Credits the workspace holds. `paid_credits + bonus_credits` adds up to this (0 when it is 0 or below) whenever both are known. |
| `paid_credits` | number or null, required | The part you bought or were granted; it never expires. Null when we could not read the breakdown just now. |
| `bonus_credits` | number or null, required | The part included with this billing period, used first; it expires at `bonus_expires_at`. Null when we could not read the breakdown just now. |
| `bonus_expires_at` | string or null, required | When `bonus_credits` expire (ISO 8601), or null. |
| `pending_credits` | number, required | This period's usage that is not yet taken off `credits`. |
| `available_credits` | number, required | `credits` minus `pending_credits`, never below 0: what you can still spend now. A send or a campaign that needs more is `402 insufficient_credits`. |
| `suspended` | boolean, required | True when sends and calls are refused (`402 workspace_suspended`). |
| `suspended_reason` | string or null, required<br>`credit_exhausted`, `subscription_past_due`, `subscription_canceled`, `no_billing_record` | Why the workspace is suspended; null when it is not. |
| `tier` | integer, required | What the account may do: `0` no card, `1` a card on file, `2` subscribed. A send that needs a card is `402 billing_not_eligible` at tier 0. |
| `has_payment_method` | boolean, required |   |
| `auto_recharge_disabled` | boolean, required | True when automatic top-up was switched off after three failed charges. Turn it back on in the dashboard under Settings, Billing. |
| `plan` | object or null, required | The subscription plan, or null with none. |
| `plan.id` | string, required |   |
| `plan.name` | string, required |   |
| `plan.renews_at` | string or null, required | When the plan renews (ISO 8601), or null. |
| `updated_at` | string or null, required | When the balance was last read from the billing system (ISO 8601): at most about 5 minutes before the request. Null when it never was. |

</div>

Example: see [Credits and balance](/platform-api/billing/#credits-and-balance).

### UsageReport

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `usage_report` |   |
| `from` | string, required | The window's start, inclusive, as read (ISO 8601 UTC, microseconds). |
| `to` | string, required | The window's end, exclusive, as read (ISO 8601 UTC, microseconds). |
| `cycle_start` | string, required | The current billing cycle's start (the default `from`) (ISO 8601 UTC, microseconds). |
| `cycle_end` | string or null, required | When the current billing cycle ends (ISO 8601), or null when the workspace has no billing period (the cycle is then the last 30 days). |
| `category` | string or null, required<br>`phone`, `recording`, `speech`, `ai`, `messaging`, `whatsapp`, `email`, `numbers`, `other` | The `category` filter, or null. |
| `group_by` | array of string, required<br>each `day`, `category`, `label` |   |
| `total_credits` | number, required | Credits charged in the window (rounded to 4 decimals). The groups' `credits` add up to it, give or take the rounding of each group. |
| `events` | integer, required | Charges in the window: the rows `/v1/usage/events` and `/v1/usage/export` answer for the same `from`, `to` and `category`. |
| `groups` | array of [UsageGroup](#usagegroup), required | Day ascending, then credits descending. At most 5,000 (`400 too_many_groups` above). |

</div>

Example: see [Usage and spend](/platform-api/billing/#usage-and-spend).

### UsageGroup

<div class="ref-table">

| field | type | description |
|---|---|---|
| `day` | string or null, required | The UTC day (`YYYY-MM-DD`) when `group_by` has `day`, else null. A day with no usage has no group. |
| `category` | string or null, required<br>`phone`, `recording`, `speech`, `ai`, `messaging`, `whatsapp`, `email`, `numbers`, `other` | When `group_by` has `category`, else null. |
| `label` | string or null, required | What was charged, in words (`SMS outbound (US)`, `AI Tokens (input)`), when `group_by` has `label`, else null. New labels appear as we bill new things. |
| `credits` | number, required | Credits charged, rounded to 4 decimals. |
| `units` | number or null, required | How much was used, in `unit`. Only when `group_by` has `label` and the label has one unit; null otherwise (3 messages and 3 minutes are not 6 of anything). |
| `unit` | string or null, required | What `units` count: `message`, `minute`, `1k_tokens`, `1k_chars`, `number_month`, … (new values appear). Null with `units`. |
| `events` | integer, required | How many charges the group adds up. |

</div>

```json
{
  "day": null,
  "category": "phone",
  "label": null,
  "credits": 0.83,
  "units": null,
  "unit": null,
  "events": 1
}
```

### UsageEventList

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `list` |   |
| `data` | array of [UsageEvent](#usageevent), required |   |
| `has_more` | boolean, required |   |
| `next_cursor` | string or null, required | Pass as `after` for the next page; `null` on the last. |

</div>

Example: see [Credits and balance](/platform-api/billing/#credits-and-balance).

### UsageEvent

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string, required | The charge's id (UUID). |
| `object` | string, required<br>always `usage_event` |   |
| `recorded_at` | string, required | When it was charged (ISO 8601 UTC, microseconds). |
| `category` | string, required<br>`phone`, `recording`, `speech`, `ai`, `messaging`, `whatsapp`, `email`, `numbers`, `other` | What the credits went on: `phone` (call minutes, attempts, voicemails and short calls), `recording` (call connection and call recording minutes, recording storage), `speech` (speech-to-text, text-to-speech and realtime voice), `ai` (AI model tokens and embeddings), `messaging` (SMS and MMS, and messages on chat channels (Telegram, Messenger, Instagram, LINE, Slack, Discord)), `whatsapp` (WhatsApp conversation and platform fees), `email` (email sent), `numbers` (phone number rental), `other` (anything else (knowledge-base crawls, workflow steps, new kinds of usage)). |
| `label` | string, required | What was charged, in words. |
| `units` | number, required | How much was used, in `unit`. |
| `unit` | string or null, required | What `units` count, or null when unknown. |
| `credits` | number, required | Credits charged (4 decimals). |
| `conversation_id` | string or null, required | The conversation it was for (UUID, as `GET /v1/conversations/{id}` takes), or null. |
| `agent_id` | string or null, required | The agent (`ag-…`), or null. |
| `campaign_id` | string or null, required | The campaign (its UUID, as a call's `campaign_id`), or null. |

</div>

Example: see [Credits and balance](/platform-api/billing/#credits-and-balance).

### OutboundCompliance

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `outbound_compliance` |   |
| `accepted` | boolean, required | True when the current text is accepted. Until then a campaign start is `409 outbound_compliance_required` (a preview reports the same code); drafts are not gated. |
| `version` | string or null, required | The version this workspace accepted last, or null if none. |
| `current_version` | string, required | The version of `attestation`: send it to `POST /v1/compliance/accept`. |
| `attestation` | string, required | The text an owner or admin accepts. |
| `accepted_at` | string or null, required | When `version` was accepted (ISO 8601), or null. |

</div>

Example: see [Accept the outbound compliance acknowledgment](/platform-api/campaigns/#accept-the-outbound-compliance-acknowledgment).

### ComplianceAccept

<div class="ref-table">

| field | type | description |
|---|---|---|
| `version` | string, required<br>≥ 1 chars | The `current_version` you read from `GET /v1/compliance`: you accept the text you were shown. |

</div>

```json
{
  "version": "2026-09-01"
}
```