# List legs

`GET /v1/voice/legs`

Returns a paginated list of the workspace's legs, ordered by start time
descending.

The `status` filter selects where in the lifecycle you look, and any
combination is a single page: in-flight statuses (`ringing`,
`in_progress`), final ones, or both together. Omit it and you get
completed legs, which is what this list has always returned.

A leg in flight carries no economics yet: `duration_ms`, `billable_ms`,
`ended_at`, and `cost` are null until it ends. It keeps the same `id`
throughout, so the same leg answers under one identity from the first
ring to settlement.

## Code samples

**TypeScript**

```ts
for await (const leg of bird.voice.legs.list()) {
  console.log(leg.id, leg.status);
}
```

Examples: [TypeScript](/docs/api/reference/list-voice-legs.ts.md) · [Python](/docs/api/reference/list-voice-legs.py.md) · [Go](/docs/api/reference/list-voice-legs.go.md) · [PHP](/docs/api/reference/list-voice-legs.php.md) · [CLI](/docs/api/reference/list-voice-legs.cli.md) · [MCP](/docs/api/reference/list-voice-legs.mcp.md) · [cURL](/docs/api/reference/list-voice-legs.curl.md)

## Example response `200`

```json
{
  "data": [
    {
      "id": "vcl_01krdgeqcxet5s7t44vh8rt9mg",
      "call_id": "vcs_01krdgeqcxet5s7t44vh8rt9mg",
      "workspace_id": "ws_01krdgeqcxet5s7t44vh8rt9mg",
      "direction": "outbound",
      "from": "+14155551234",
      "to": "+16505559876",
      "actor": {
        "id": "usr_01krdgeqcxet5s7t44vh8rt9mg",
        "type": "user"
      },
      "sip_trunk_id": "spt_01krdgeqcxet5s7t44vh8rt9mg",
      "status": "answered",
      "sip_response_code": 200,
      "rejection_reason": "destination_not_enabled",
      "route": {
        "type": "reject"
      },
      "tags": [
        {
          "name": "category",
          "value": "welcome"
        }
      ],
      "duration_ms": 65000,
      "pdd_ms": 850,
      "billable_ms": 60000,
      "media_quality": {
        "mos": 4.32,
        "jitter_ms": 12,
        "packet_loss_pct": 1.5,
        "round_trip_time_ms": 42
      },
      "cost": {
        "amount": "0.013000",
        "currency_code": "USD",
        "outbound_amount": "0.013000",
        "inbound_amount": null,
        "call_handling_amount": null,
        "recording_amount": null,
        "transcription_amount": null
      }
    }
  ],
  "next_cursor": "eyJ2IjoxLCJzIjoiXCIyMDI2LTA1LTI1VDE0OjAzOjEwWlwiIiwiaSI6IjAxOTJmM2IxLTRjN2UtN2EyYi05ZDYxLThmM2E1YzJlN2I0MCJ9",
  "prev_cursor": null,
  "refresh_cursor": "eyJ2IjoxLCJzIjoiXCIyMDI2LTA1LTI1VDE2OjQyOjAxWlwiIiwiaSI6IjAxOTJmM2IxLTllMDQtN2NkMy1iODE3LTJhNmY0ZDFjOGUwOSJ9"
}
```

## Query parameters

- `direction` (string)

  Return only legs in this direction.

  Possible values: `inbound`, `outbound`
- `status` (array)

  Return only legs with one of these statuses, comma-separated.
  In-flight and final statuses may be combined freely.
- `call_id` (string): Return only legs belonging to this call, which is how the legs of one multi-party or transferred call are correlated.
- `sip_trunk_id` (string): Return only legs carried by this SIP trunk.
- `from` (string): Return only legs placed from this calling party number, matched as a whole number rather than as a fragment. Give it in international form: `+14155551234`, `14155551234`, and `0014155551234` all select the same legs. A number given without a country code is read as an international one, so give the country code to be sure of what you are matching. Use `number` instead to match part of a number, or either side of the leg.
- `to` (string): Return only legs placed to this called party number, matched as a whole number rather than as a fragment. Give it in international form: `+16505559876`, `16505559876`, and `0016505559876` all select the same legs. A number given without a country code is read as an international one, so give the country code to be sure of what you are matching. Use `number` instead to match part of a number, or either side of the leg.
- `number` (string): Return only legs where the calling or called number contains this value. Matches a partial number, so a country or area-code prefix returns every leg to or from it. Combines with `from`/`to`, which match one side exactly.
- `tag` (array): Filter by tag. Accepts `name` to match any record carrying that tag name, or `name:value` to match a specific tag pair (for example `category:welcome`). Repeat the parameter to add more tags. A record must match every tag listed to be returned.
- `started_after` (string): Return only legs that started at or after this instant, inclusive. RFC 3339 timestamp.
- `started_before` (string): Return only legs that started at or before this instant, inclusive. RFC 3339 timestamp.
- `limit` (integer): Maximum number of items to return per page.
- `starting_after` (string): Cursor from the `next_cursor` field of a previous list response. Returns items immediately after the cursor position in the current sort order.
- `ending_before` (string): Cursor from the `prev_cursor` or `refresh_cursor` field of a previous list response. Returns items immediately before the cursor position in the current sort order. `prev_cursor` returns the preceding page. `refresh_cursor` anchors at the first row of that response, which on a newest-first sort is how to fetch the items that have appeared since.

## Response body

- `data` (array of object, required)
- `data.id` (string, required): Unique identifier for this leg record.
- `data.call_id` (nullable string): Call identifier shared across all legs of a multi-party or transferred call. Use this to correlate related leg records. `null` when call correlation is not available for the leg.
- `data.workspace_id` (string, required)
- `data.direction` (string, required)
- `data.from` (string, required): Calling party number in E.164 format.
- `data.to` (string, required): Called party number in E.164 format.
- `data.actor` (object): Who placed the leg: the API key whose credentials it used, the integration acting for the workspace, or the user who placed it from a browser or the CLI. Absent when the leg was admitted only by its source IP address, or when no actor was recorded.
- `data.actor.id` (string, required): Actor identifier.
- `data.actor.type` (string, required)

  New actor types may be added. Treat unrecognized values as future types, not errors.
  - `user`: a member's own session.
  - `api_key`: a workspace API key.
  - `oauth_token`: a token issued to a caller on a member's behalf.
  - `system`: an action we perform without a customer actor.
  - `sso`: an organization's SSO connection.
  - `service_account`: a workspace's connected Integration acting with no member behind it.
  - `automation`: an automation execution in your workspace.

  Possible values (may grow over time): `user`, `api_key`, `oauth_token`, `system`, `sso`, `service_account`, `automation`
- `data.actor.display_name` (nullable string): The label the actor is shown under: typically a member's name or email address, or the API key's name. Null when it could not be resolved.
- `data.sip_trunk_id` (nullable string): Identifier of the SIP trunk that originated this leg. `null` when no trunk is associated.
- `data.status` (string, required)
- `data.sip_response_code` (nullable integer): Final SIP response code received from the carrier. `null` when no SIP response was received, for example on timeout or DNS failure.
- `data.rejection_reason` (string)

  Why we rejected the leg. Absent on connected legs and legs rejected
  by the carrier or recipient. For carrier or recipient rejections, see
  `sip_response_code`; a `6xx` decline gives the leg a `rejected` status.

  Read alongside `route` when present. A refusal caused by the number's
  configuration has no rejection reason; the route records that
  configuration.
- `data.route` (object): Which answer your number gave an incoming leg: a SIP trunk, a forward, or a refusal. Recorded when the leg was handled, so changing the number's setup afterwards does not change what its past legs say. Absent on outbound legs, and on legs recorded before this field existed.
  - Variant `object`
    - `data.route.type` (string, required): The number turned the leg away. This is where every number starts, so it covers a number nobody has configured as well as one set to reject.
  - Variant `trunk_id`
    - `data.route.type` (string, required): The leg was delivered to one of your SIP trunks.
    - `data.route.trunk_id` (string, required): The SIP trunk the leg was delivered to. Recorded as it was at the time, so it may name a trunk you have since changed or deleted.
  - Variant `forward_to + forward_as`
    - `data.route.type` (string, required): The leg was forwarded to another of your numbers.
    - `data.route.forward_to` (string, required): The number the leg was forwarded to, in E.164 format. Recorded as it was at the time, so it may name a number you have since stopped verifying.
    - `data.route.forward_as` (string, required): Which of the leg's two numbers the forwarded leg presented as its caller. The value that went on the wire, not the one the number is set to now.
- `data.tags` (array of object): Your own `{name, value}` labels for this leg, taken from the `X-Bird-Call-Tag` headers on the INVITE that placed it. Set them to organise legs by a dimension of your own (campaign, queue, agent, cost centre), then filter this list by them with `tag`. Read-only here: a leg is labelled when it is placed, and never afterwards. What is here may be less than what was sent, and the leg still goes through either way: a tag whose name or value breaks the rules below is dropped, anything past the first five is ignored, and a name sent more than once keeps its first value. Absent when the leg carried none, and on legs recorded before this field existed.
- `data.tags.name` (string, required): Tag name. ASCII letters, digits, underscore, and hyphen only. Case-sensitive. Maximum 32 characters.
- `data.tags.value` (string, required): Tag value. ASCII letters, digits, underscore, and hyphen only. Case-sensitive. Maximum 64 characters.
- `data.started_at` (string, required): When the leg was initiated.
- `data.answered_at` (nullable string): When the leg was answered (`200` OK received). `null` for unanswered legs.
- `data.ended_at` (nullable string): When the leg ended (BYE or final non-2xx response). `null` for legs that ended abnormally without a recorded end event.
- `data.duration_ms` (nullable integer): Total leg duration in milliseconds, measured from the first INVITE to the BYE or final response. `null` while the leg is still in progress and has no final duration yet.
- `data.pdd_ms` (integer): Post-dial delay in milliseconds: how long the caller heard nothing between dialing and the phone starting to ring at the other end. High values are what callers experience as the leg `not going through`. Absent when the leg never rang, either because it failed first or because the carrier answered it immediately.
- `data.billable_ms` (nullable integer): Billable duration in milliseconds, measured from answer to leg end. Zero for unanswered legs, and `null` while the leg is still in progress.
- `data.media_quality` (object): How the audio sounded, as opposed to whether the leg connected. Absent when the leg carried no audio, or when the far end reported nothing to measure from.
- `data.media_quality.mos` (number, required): Mean opinion score, the single number for how the call sounded, from 1 (unintelligible) to 5 (as good as being in the same room). Anything at or above 4.0 is what most people would call a clear line, and below 3.5 is where callers start asking each other to repeat themselves. The three other fields are the impairments that move it.
- `data.media_quality.jitter_ms` (integer, required): Variation in the arrival time of the audio packets, in milliseconds. Audio arriving unevenly is heard as choppiness even when no packets are lost at all.
- `data.media_quality.packet_loss_pct` (number, required): Percentage of audio packets that never arrived. Heard as brief gaps or clipped words, and the impairment that degrades a call fastest.
- `data.media_quality.round_trip_time_ms` (integer, required): Round-trip time between the two ends, in milliseconds. It does not distort the audio. Above roughly 300 ms, the two parties start talking over each other.
- `data.cost` (object): What the leg cost, net of tax, at full precision, split into the components that make it up. Absent until the leg has been rated; unanswered or unpriced legs have no cost.
- `data.cost.amount` (string, required): Total charged, as a decimal string: the sum of the components below. Net of tax, which applies to your wallet balance rather than to an individual charge.
- `data.cost.currency_code` (string, required): ISO 4217 currency code. Every component is denominated in this currency.
- `data.cost.outbound_amount` (nullable string, required): What we charged to carry the leg to the destination network, as a decimal string. `null` until this component is priced.
- `data.cost.inbound_amount` (nullable string, required): What we charged to receive the leg from the originating network, as a decimal string. Only a leg that arrived at your number can carry it. `null` until this component is priced.
- `data.cost.call_handling_amount` (nullable string, required): What we charged for handling the call itself, as a decimal string. A call is charged for handling once, however many legs it has, so only one leg's record carries it. `null` until this component is priced.
- `data.cost.recording_amount` (nullable string, required): What we charged to record the leg, as a decimal string, billed per second over the same billable time as the rest of the leg. `null` until this component is priced.
- `data.cost.transcription_amount` (nullable string, required): What we charged to transcribe the leg's audio, as a decimal string, billed per second of recorded audio rather than for the length of the leg. A transcript is produced after the leg ends, so this can appear after the rest of the cost. `null` until this component is priced.
- `next_cursor` (nullable string, required): Cursor for the next page. Pass back as `starting_after` to advance forward. `null` when no next page exists.
- `prev_cursor` (nullable string, required): Cursor for the previous page. Pass back as `ending_before` to step backward. `null` when no previous page exists.
- `refresh_cursor` (nullable string, required): Refresh anchor, the first row of this response. Pass back as `ending_before` to fetch what precedes it in the current sort order. On a newest-first sort those are the items that have appeared since; on any other sort they are the items that sort earlier, so refreshing such a list means re-fetching it instead. Non-`null` whenever `data` is non-empty; `null` only on an empty page. Distinct from `prev_cursor`.

## Related resources

- [What is a voice API?](/explained/voice/what-is-a-voice-api) (answer)
- [Voice](/products/voice) (product)
- [Voice overview](/docs/guides/voice/overview) (docs)

[Get an implementation brief](/learn/workspace?topic=voice)
