---
title: "Usage feed of bridge conversations, with the billed flag per row"
method: GET
path: "/voice/bridge/usage"
tags: ["voice"]
---

# Usage feed of bridge conversations, with the billed flag per row

`GET /voice/bridge/usage`

## Request

- Base URL: `https://corebee.ai/api/v1`
- URL: `https://corebee.ai/api/v1/voice/bridge/usage`
- Auth: HTTP bearer

The bulk read for your own reporting. GET /voice/bridge/{id} returns ONE session; this returns MANY, date-filtered, each with the billed flag and amount read from the append-only usage ledger rather than inferred from the state string, so this feed reconciles with the invoice by construction.

NOT the {data: [], meta} list envelope used elsewhere in this API: `data` is a single OBJECT carrying the window, the counts and a `conversations` array. Offset pagination, not the keyset cursor used by GET /voice/bridge.

Two rows are filtered out server-side, from the `total` as well as the page: briefing-only test sessions (the consumer leg is never dialed on those), and anything created before the pilot start date. A window that falls entirely before the pilot start is an empty 200, never an error.

Only reporting-safe fields are returned. Internal identifiers (org_id, api_key_id, phone_channel_id, idempotency_key), the prep-line text and raw carrier event payloads are never exposed here.

## Query parameters

- `from` string
- `to` string
- `limit` integer
- `offset` integer
- `customer_ref` string

## Response `200`

OK

- object
  - `data` BridgeUsageFeed, required: One page of the usage feed. `total` counts every row matching the window AFTER the briefing-only and pilot-start exclusions, so total and conversations are always drawn from the same population.
    - `range` object, required: The window actually applied, AFTER defaults and the pilot-start floor. Echoed back precisely because `from` may not be the value you sent.
      - `from` string, date-time, required
      - `to` string, date-time, required
    - `total` integer, required: Rows matching the window, not just this page.
    - `limit` integer, required: Page size after clamping.
    - `offset` integer, required
    - `returned` integer, required: Rows in `conversations` on this page.
    - `conversations` BridgeUsageConversation[], required
      - `id` integer
      - `created_at` string, date-time, nullable
      - `bridged_at` string, date-time, nullable: When the bridge was RECORDED, which is when our webhook worker processed the carrier notification, not the instant the two legs were joined. It is written from server time after queue lag, so it can trail the true join by several seconds and must not be used as a billing start time or differenced against a hangup to compute duration.
      - `connected_at` string, date-time, nullable: When the two people were ACTUALLY joined, taken from the carrier's own call.bridged stamp. This is the field to use for a connection time; `bridged_at` is when we wrote it down. The gap between them is our ingest lag: a median of 7.73s across every bridge in the product's history, and on three of them `bridged_at` lands 0.693s, 7.736s and 10.210s AFTER the call had already ended. This field carries a hard guarantee that `bridged_at` never did: it can never be later than the end of the call. When the carrier stamps do not survive that check, or the evidence pass has not run yet, it is null, it is never guessed and never falls back to `bridged_at` or to a server clock.
      - `state` 'created' | 'dialing_business' | 'ivr_nav' | 'amd_verdict' | 'human' | 'voicemail' | 'prep_line' | 'converse' | 'dialing_consumer' | 'bridging' | 'bridged' | 'completed' | 'failed'
      - `billed` boolean: True when a row exists for this session in the append-only usage ledger AND the row is not seeded demo content. CHANGED 2026-08-21, and it is the only field on this feed whose value moved: eleven seeded demo rows that used to report billed:true now report false. Sandbox magic-number rows are unchanged and still report billed:true (see `sandbox`). Live rows are unchanged. For "may appear on an invoice" prefer `invoiceable`, which is strictly narrower, `billed` says a ledger row exists, `invoiceable` says no billing rule excluded it.
      - `amount_cents` integer, nullable: Ledger amount in cents, or null when no ledger row exists. ⛔ NOT ZERO WHEN `billed` IS FALSE. A seeded demo row (`mode:"demo"`) has a real ledger row and therefore a real amount, and we still show you both, a hidden row is indistinguishable from a lost one. What changed on 2026-08-21 is the CLAIM we make about it, not its visibility. Summing this field alone therefore still includes demo rows. For revenue, sum it filtered on `invoiceable`, or filter on `mode == "live"`.
      - `mode` 'live' | 'sandbox' | 'demo': What KIND of traffic this row is. THIS is the field to filter on. - `live`, real partner traffic, the only mode that can be invoiced. - `sandbox`, a reserved +1500555 magic-number test you ran. A real row, shaped like a real one on purpose, not revenue. - `demo`, seeded sample data. Never a phone call, never revenue, and it was never one of yours. Agrees with the older `sandbox` boolean on every row except one carrying BOTH markers, where mode says `demo` because a seeded row is not something you ran. Treat an unrecognized value as not-live rather than switching exhaustively.
      - `invoiceable` boolean: Whether this row can appear on an invoice: live, ledgered, and excluded by no billing rule. Strictly narrower than `billed`. Sum this, not `state`, not `billed`.
      - `joined_seconds` number, nullable: Seconds of JOINED audio: how long two humans were actually on the line together. Read from the carrier's own call.bridged start_time, NOT from `bridged_at`, that field is stamped by our webhook worker a median 7.73s after the fact and on three historical sessions post-dates the hangup, which is why differencing it yields negative durations. null when the carrier stamps are missing or contradictory. Never negative, and never guessed from a clock.
      - `short_duration` boolean: joined_seconds is under 6 seconds. PURELY INFORMATIONAL, a short connected call is still billed, exactly as our own carriers bill us (Telnyx applies a 60-second minimum, Twilio a 1-second floor). Do not read this as a waiver; it is a flag for you to look at, not a deduction.
      - `bridge_evidence` 'two_legs_confirmed' | 'one_leg_confirmed' | 'command_accepted_only', nullable: What evidence promoted this session to `bridged`. - `two_legs_confirmed`, two call.bridged notifications observed. Both legs really joined. - `one_leg_confirmed`, exactly one was observed. - `command_accepted_only`, none were. The only evidence is that the carrier ACCEPTED our bridge command, which is a promise to try rather than a witness that anyone connected. null until the evidence pass has run over the session, which is the normal state for a row read seconds after it completed. null is not a verdict.
      - `unbilled_connection` boolean: True when we can PROVE two people were connected on this session and no ledger row exists for it, i.e. we owe you the connection and never charged for it. It requires the strongest evidence level (`bridge_evidence == "two_legs_confirmed"`), so it can never be raised by a session that merely tried to bridge, and it is never true for sandbox or demo rows. Expect it to be false on essentially every row; a true is a reconciliation item, not a normal state. Rows are surfaced rather than corrected in place because the ledger is append-only and inserting into it is a deliberate, audited action.
      - `sandbox` boolean: True when the conversation was produced by a reserved magic test number. Such a row is returned billed:true and shaped exactly like a real one ON PURPOSE, so you can code against the real billed shape before a real call happens, but it is NOT invoiced. Exclude it from any total you compute. True for every magic outcome, connected or not.
      - `customer_ref` string, nullable: Your own correlation id from metadata.customer_ref at create time, or null.
      - `company_id` string, nullable: The identifier to group by: customer_ref when you supplied one, else the business number dialed.
      - `business_number` string, nullable
      - `consumer_number` string, nullable

## Other responses

- `400`: Validation error
- `401`: Invalid or missing credential
- `403`: Forbidden (missing scope, or resource not owned by the credential org)
- `429`: Rate limit exceeded (code rate_limit_exceeded). The ceiling is 100 requests per minute per API key. The response carries Retry-After (seconds) plus X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset, and repeats the seconds in the body as retry_after.
- `500`: Error

---

[API](https://skmtc.dev/corebee/apis/corebee-api.md) · [All operations](https://skmtc.dev/corebee/apis/corebee-api/llms.txt) · [OpenAPI document](https://skmtc.dev/corebee/apis/corebee-api/revisions/a48ea543b4dc?raw)
