# Billing: usage, call records, balance, ledger

Read-only views of what the project spends. Everything is the project in the path only. Top-ups and payment
methods stay in the portal. Paths are relative to `https://api.contactivity.io/v1/projects/{project_id}`; see the
[overview](/guides/overview.md). Scope: `billing:read`.

| Method | Path | Purpose |
|---|---|---|
| GET | `/usage` | Usage and cost by product over a period |
| GET | `/cdrs` | Call detail records (paged) |
| GET | `/balance` | Current balance |
| GET | `/ledger` | Balance changes (paged) |

`from` and `to` (on `/usage` and `/cdrs`) take RFC 3339 or `YYYY-MM-DD` (00:00 UTC). `from` defaults to the start
of the current UTC month, `to` to one month after `from`; the range is `[from, to)` and at most 366 days
(`400 invalid_parameter`, param `from` or `to`).

## Usage

```sh
curl "https://api.contactivity.io/v1/projects/$PROJECT_ID/usage?from=2026-10-01&to=2026-11-01" \
  -H "Authorization: Bearer $CONTACTIVITY_API_KEY"
```

```json
{
  "from": "2026-10-01T00:00:00.000Z", "to": "2026-11-01T00:00:00.000Z",
  "products": [
    {"product": "voice_outbound", "count": 412, "duration_secs": 30215, "cost": {"amount": "4.1230", "currency": "USD"}},
    {"product": "ai_agent", "count": 37, "duration_secs": null, "cost": {"amount": "9.8800", "currency": "USD"}}
  ],
  "total_cost": {"amount": "14.0030", "currency": "USD"}
}
```

Products are `voice_inbound` and `voice_outbound` (from the call records) and the ledger types of other charges
(AI agent minutes, number fees, features). Errors: `400 invalid_parameter`, `503 usage_unavailable`.

## Call detail records

`GET /cdrs` lists rated call legs, newest first. Filters: `from`, `to`, `direction` (`inbound`, `outbound`),
`status` (`all`, `answered`, `missed`), `number` (digits matched anywhere in the caller or destination),
`min_sec`, `min_cost`, `limit`, `starting_after`.

| Field | Type | Description |
|---|---|---|
| `id` | string | |
| `sip_call_id` | string | The leg's SIP Call-ID |
| `direction` | string | `inbound` or `outbound` |
| `from`, `to` | string | |
| `started_at`, `answered_at` | timestamp | `answered_at` null when unanswered |
| `answered` | boolean | |
| `duration_secs` | integer | |
| `price` | money or null | |
| `hangup_cause` | string | |
| `quality` | object or null | Media quality per leg |

Errors: `400 invalid_parameter`, `503 cdrs_unavailable`.

## Balance

`GET /balance` returns `{"balance": {"amount": "125.40", "currency": "USD"}}` (2 decimals). Errors:
`503 balance_unavailable`.

## Ledger

`GET /ledger` lists balance changes, newest first; filter `type` (for example `topup`, `refund`, `adjustment`,
`ai_agent`, `number_monthly`).

| Field | Type | Description |
|---|---|---|
| `id` | string | |
| `type` | string | |
| `amount` | money | Always positive |
| `direction` | string | `credit` or `debit` |
| `description` | string or null | |
| `status` | string | `succeeded`, or `pending` until applied to the balance |
| `created_at` | timestamp | |

Prices per destination are under `GET /pricing` ([voice-apps.md](/guides/voice-apps.md#pricing)).
