# 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": "programmable_voice_outbound", "count": 412, "duration_secs": 30215, "cost": {"amount": "4.1230", "currency": "USD"}}
  ],
  "total_cost": {"amount": "4.1230", "currency": "USD"}
}
```

Call lines are `<product>_inbound` and `<product>_outbound` for each [product](#products), for example
`sip_trunking_outbound` (from the call records); other lines are the ledger types of other charges
(number fees, features). Errors: `400 invalid_parameter`, `503 usage_unavailable`.

## Products

Each call leg is billed on its own, as one product:

| Product | Legs |
|---|---|
| `sip_trunking` | Between your SIP trunk or PBX and the phone network |
| `programmable_voice` | Phone network legs handled by a voice app (CoXML, Call Control, flow) or the Calls API |
| `programmable_voice_sip` | Voice app legs that never reach the phone network: SIP addresses, in-app clients, your own trunks. One rate for every destination |
| `byoc_sip_trunking` | Trunk legs through [your own carrier](/guides/byoc.md). Platform fee only, the same both ways |
| `byoc_programmable_voice` | Voice app legs through your own carrier. Platform fee only, the same both ways |

A call that uses several of them is billed for each leg: a number routed to a voice app that dials your trunk is
one `programmable_voice` leg and one `programmable_voice_sip` leg, and a number forwarded to a phone number is two
`sip_trunking` legs: inbound at the number's inbound rate and outbound at the forwarding destination's rate, both for
the whole call (a [BYOC](/guides/byoc.md) number forwarded through your carrier: two `byoc_sip_trunking` legs, the fee each way). Legs are billed per 60 seconds, rounded up,
unless your agreement says otherwise; each rate's increment is under [`GET /pricing`](/guides/voice-apps.md#pricing).

Calls between Contactivity numbers stay on our network and are billed at the `programmable_voice_sip` rate, never
at phone network rates. A number forwarded to another Contactivity number (yours or another customer's) pays its
inbound leg as usual and a `programmable_voice_sip` leg to that number; the number it reaches pays a
`programmable_voice_sip` inbound leg on its own project. In a chain of forwards (A to B to C) each number pays for its own legs:
A its `programmable_voice_sip` leg out, B one in and one out, C one in; a chain that ends at a phone number pays
that outbound leg at its `sip_trunking` rate. When the project a call would be billed to on the receiving side has no
credit, the caller hears that the number is not available (`480`); your own calls out of a project without credit
get `402`. A call that goes on to a route's fallback route is checked against your balance again before it rings,
at the fallback's price; without enough credit it ends with `480`.

Transactions show amounts to 4 decimals, rounded up: a charge below $0.0001 shows as $0.0001. Your balance and
the amounts we store keep 6 decimals. A call from your trunk or a voice app to a Contactivity number is a `programmable_voice_sip`
leg for you and one for the number's project. More than 3 numbers forwarding in a row is a loop: the call is
refused with `482 Loop Detected` and nothing is charged.

## Standard prices

`GET https://api.contactivity.io/v1/public/pricing` returns the platform's standard prices, with no API key: voice
per [product](#products) (`sip_trunking` and `programmable_voice` per +1 region and direction, the others one rate
per minute), and US and Canada local and toll-free numbers. It never includes your own terms;
your project's prices are [`GET /pricing`](/guides/voice-apps.md#pricing).

```sh
curl "https://api.contactivity.io/v1/public/pricing"
```

```json
{
  "currency": "USD", "billing_increment": "60/60",
  "voice": {
    "sip_trunking": {"regions": [{"id": "us48", "name": "US48",
      "outbound": {"rate_per_min": {"amount": "0.009000", "currency": "USD"}, "increment": "60/60"},
      "inbound": {"rate_per_min": {"amount": "0.005000", "currency": "USD"}, "increment": "60/60"}}]},
    "programmable_voice_sip": {"rate_per_min": {"amount": "0.004000", "currency": "USD"}, "increment": "60/60"},
    "byoc_programmable_voice": {"rate_per_min": null, "increment": "60/60"}
  },
  "numbers": [{"country": "US", "type": "local", "available": true,
    "setup_fee": {"amount": "1.000000", "currency": "USD"}, "monthly_fee": {"amount": "1.150000", "currency": "USD"}}]
}
```

A price that is not set is `null`; a number with `"available": false` is not sold. Responses are cached for 5 minutes (`Cache-Control: public, max-age=300`)
and rate limited per IP address (`429` with `Retry-After`).

Each region also lists its `prefixes`, the +1 area codes it covers (`+1` holds every area code not in another
region). `features` has the call feature prices: `transcription` and `noise_suppression` per minute,
`machine_detection` and `premium_machine_detection` per attempt (`price_per_attempt`), `recording` per recorded minute and
`recording_storage` per stored minute per month (`rate_per_min_month`) after `free_days` (see
[Recording and storage](#recording-and-storage)), `live_transcription` per minute (see
[Live transcription](#live-transcription)), and `speech_recognition` per 15 seconds (`price_per_15_secs`) only
when it is priced. A detection attempt is
charged each time detection runs on a call, whatever the result.

### International prices

`GET https://api.contactivity.io/v1/public/pricing/international?q=` searches the standard international outbound
prices of `sip_trunking` and `programmable_voice`, also with no API key. `q` is a destination name (2-60
characters, matched anywhere in the name), a prefix such as `+44`, or a whole number, which returns every prefix of
it. Up to 100 destinations, by name; `has_more` is `true` when more match. A product without a standard price for a
destination is `null`. Destinations under a blocked prefix are left out, and your own terms are never included.

```sh
curl "https://api.contactivity.io/v1/public/pricing/international?q=united%20kingdom"
```

```json
{
  "currency": "USD", "billing_increment": "60/60", "has_more": false,
  "destinations": [{"prefix": "+44", "destination": "United Kingdom",
    "sip_trunking": {"rate_per_min": {"amount": "0.020000", "currency": "USD"}, "increment": "60/60"},
    "programmable_voice": {"rate_per_min": {"amount": "0.025000", "currency": "USD"}, "increment": "60/60"}}]
}
```

Calling these destinations needs account tier 3 and an approved international calling request
(`POST /international-requests`). Errors: `400 invalid_parameter` (`q` missing, too short or too long),
`429 rate_limit`. Cached for 5 minutes, like the standard prices.

## Recording and storage

Call recordings are a software charge, separate from call minutes (ledger types `recording` and
`recording_storage`):

- **Recording**: charged once when a recording completes, whatever started it (`<Record>`, `<Dial record>`,
  `record_start`, conference and trunk recording, voicemail): its duration rounded up to the billing
  increment (60/60) at the recording price per minute. A dual-channel recording counts its minutes once. The
  charge is the recording's `price` and shows in the call's pricing.
- **Storage**: free for `free_days` (90) from the recording's creation. After that, each calendar month (UTC) a
  stored recording is charged for the whole month: its billed minutes times the storage price per minute-month,
  once per recording and month, at the first hourly run after its free period ends. Archived recordings are
  charged too: recordings are never deleted. Billing contacts get one notice per project and month.

Amounts are rounded to 4 decimals; a charge that rounds to $0 is not booked. Organizations can have their own
recording and storage prices; [`GET /pricing`](/guides/voice-apps.md#pricing) shows them under `recording`.

## Live transcription

Live transcription (`<Start><Transcription>`, `transcription_start`) is charged per minute of streamed audio,
rounded up to the billing increment, once per session and track (ledger type `live_transcription`). Dual-track
transcription counts each track. Organizations can have their own price, shown under `recording.live_transcription`
in [`GET /pricing`](/guides/voice-apps.md#pricing). Speech recognition in a speech `<Gather>` (ledger type
`speech_recognition`) is charged per 15 seconds, at least one block per gather, when priced.

## Call detail records

`GET /cdrs` lists rated call legs, newest first. Filters: `from`, `to`, `direction` (`inbound`, `outbound`), `product`,
`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 | |
| `product` | string | See [Products](#products) |
| `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`,
`number_monthly`, `recording`, `recording_storage`, `live_transcription`, `speech_recognition`).

| 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)).
