What actually happened on your channels.

```ts
const { calls } = await connect.calls.list({ workspaceId, from, to });
const call = await connect.calls.get(callId);
// call.transcript, call.outcome, call.durationSeconds, call.extraction
```

A call record carries the outcome, the duration, the transcript, any tool calls the agent made, and whatever post-call extraction the agent was configured to perform.

**Transcripts are the customer's data.** They contain whatever the caller said - names, card numbers, medical details, whatever the conversation was about. Whether a call is recorded at all is the workspace's setting, not yours, so treat an absent transcript as normal rather than as a failure. Whatever you store from here falls under the deletion policy you declared for this application.

Records appear after a call completes, not during it. There is no live stream on this API.

## Endpoints

### GET `/api/v1/application/calls`

List your calls

**Scope** `calls:read` &nbsp;·&nbsp; **Rate limit** 30 requests / min per IP, 60 requests / min per API key

Every call that arrived on a channel assigned to your application, newest first, across all of your subscribing workspaces.

The application is resolved from your API key and is never a parameter, so there is no request that reads another partner's calls. `channelId` only narrows within what you already hold; one belonging to someone else simply matches nothing.

**Calls your UI placed are a separate list.** A call a member places with `<ConnectCallButton>` leaves from the workspace's own number, which is usually assigned to no application, so it never appears above. Pass `placedByApplication=true` (optionally with `workspaceId`) for exactly those - the calls `call.completed` / `call.missed` describe with `handler: "member"`. That is what to reconcile a timeline from when a webhook was delivered somewhere else.

Otherwise `channelId` is the only filter, and that is a budget decision rather than an oversight - every filter here costs a Firestore composite index against a project already at the 200 ceiling. Filter the rest client-side, or read the aggregate from `/analytics`.

Page until `hasMore` is `false`; do not stop on an empty `items`.

What this deliberately does not return: the routing snapshot taken at pick-up (unbounded, unvalidated, and available live at `GET /channels/{channelId}`), the carrier's own call id and hangup text, and the carrier's measure of the recording's length. `agentId`/`agentVersion` carry who answered, and `durationSeconds`/`billDurationSeconds` are the two durations Connect stands behind.

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `channelId` | query | no | Narrow to one channel. |
| `placedByApplication` | query | no | `true` for the member calls your UI placed instead of the calls on your channels. |
| `workspaceId` | query | no | With `placedByApplication`, one workspace's calls. |
| `cursor` | query | no | From a previous response's `nextCursor`. |
| `limit` | query | no | 1-100, default 50. |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | Calls on your channels, newest first |
| `400` | validation_error - a limit outside 1-100 |
| `403` | forbidden - key unknown or missing the calls:read scope |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `items` | `array` | yes |  |
| `nextCursor` | `any` | yes |  |
| `hasMore` | `boolean` | yes |  |

### GET `/api/v1/application/calls/{callId}`

Get one call and its transcript

**Scope** `calls:read` &nbsp;·&nbsp; **Rate limit** 30 requests / min per IP, 60 requests / min per API key

The call record plus every transcript turn, ordered by `sequence` - which is the sort to trust, since two turns can share a millisecond.

`interrupted` marks a turn the caller talked over, and is only ever true for an `agent` turn. `extraction` is your agent's own post-call structured output, keyed by its `extractionSchema`; Connect stores it without interpreting it.

A long call's transcript is capped at 2,000 turns, the same ceiling applied when it was written.

Recording and transcript files land in the workspace's Drive: `recordingFileId` and `transcriptFileId` are Drive file ids, readable with a `drive:read` key. Both are `null` until the carrier posts the audio, which is normal for a call that has only just ended - see `recordingStatus`.

A call belonging to another application answers `404`, not `403`, for the same enumeration reason the channel reads give.

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `callId` | path | yes | From `GET /calls`. Also the id Connect carried into the recording and your own conversation record. |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | The call and its transcript |
| `403` | forbidden - key unknown or missing the calls:read scope |
| `404` | not_found - no such call, or it belongs to another application |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `call` | `object` | yes |  |
| `turns` | `array` | yes |  |

### GET `/api/v1/application/calls/availability`

Can the signed-in member call from here

**Scope** `calls:place` &nbsp;·&nbsp; **Rate limit** 30 requests / min per IP, 30 requests / min per API key

Whether the member can place a browser call right now, and if not, why - so a Call button can say what is missing instead of failing when pressed. **Member-only**, like every `calls:place` endpoint; a member without "Make calls" is refused `403`, which the SDK's bridge reports as `status: "no_permission"`.

`status`, in precedence order: `no_number` (the workspace holds no business number - `offer` names the subscription whose plan sells the "Business number" add-on), `number_pending` (one was bought but Connect's pool was empty; an admin can retry in Connect), `no_balance` (not a minute's worth of prepaid minutes), `available`.

`canTopUp` and `canManageNumber` are the member's own (`billing:update`, `subscription:update`): offer the next step only to someone who can take it. Buying minutes and adding a number both happen in Connect - link to `/calling` and to `/billing/subscriptions/{offer.subscriptionId}` with `client_id` and `return_to` so they come back.

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | The member's calling availability |
| `403` | forbidden - key missing calls:place, no session token, or the member lacks Make calls |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `status` | `available` \| `no_permission` \| `no_number` \| `number_pending` \| `no_balance` | yes |  |
| `number` | `any` | yes |  |
| `available` | `number` | yes |  |
| `availableMinutes` | `number` | yes |  |
| `ratePaisePerMinute` | `number` | yes |  |
| `lowBalance` | `boolean` | yes |  |
| `canTopUp` | `boolean` | yes |  |
| `canManageNumber` | `boolean` | yes |  |
| `offer` | `any` | yes |  |

### GET `/api/v1/application/calls/settings`

Read whether the workspace records its calls

**Scope** `calls:place` &nbsp;·&nbsp; **Rate limit** 30 requests / min per IP, 30 requests / min per API key

The workspace's call-recording switch - the same setting Connect's Calling screen edits - plus `canManage`, whether the **signed-in member** may change it (their own `channel:manage`). **Member-only:** requires `X-Connect-Session-Token`; reading needs the member's `workspace:read`.

`recordingRatePaisePerMinute` is what recording adds to every billed minute, pre-tax - show it beside the switch, because turning it on changes what every call costs.

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | The workspace's calling settings for this member |
| `403` | forbidden - key missing calls:place, or no session token |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `recordingEnabled` | `boolean` | yes |  |
| `recordingRatePaisePerMinute` | `number` | yes |  |
| `canManage` | `boolean` | yes |  |

### PUT `/api/v1/application/calls/settings`

Switch call recording on or off

**Scope** `calls:place` &nbsp;·&nbsp; **Rate limit** 30 requests / min per IP, 30 requests / min per API key

Turns recording of member calls on or off for the workspace. **Member-only**, and the member must hold `channel:manage` - the change is audited against them, not your key.

Applies to calls *started* after it: a call in progress keeps the setting, and the price, it began with. While it is on, the person called hears that the call may be recorded as they answer, and `call.analyzed` follows each recorded call. Answers the settings as they now stand.

**Request body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `recordingEnabled` | `boolean` | yes |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | The settings after the change |
| `400` | validation_error - recordingEnabled is not a boolean |
| `403` | forbidden - key missing calls:place, no session token, or the member lacks channel:manage |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `recordingEnabled` | `boolean` | yes |  |
| `recordingRatePaisePerMinute` | `number` | yes |  |
| `canManage` | `boolean` | yes |  |

### GET `/api/v1/application/calling/readiness`

Read whether the workspace may legally make each kind of call

**Scope** `calls:place` &nbsp;·&nbsp; **Rate limit** 30 requests / min per IP, 30 requests / min per API key

Where the workspace stands on Connect's calling compliance journey - the steps Indian telecom rules ask of a business before it calls (DLT registration, a 140-series number with an approved voice header for sales calls, the autodialer notice to the operator, consent, calling hours). Two callers: a signed-in member (`X-Connect-Session-Token`; reading needs their `workspace:read`), or your own server with `?workspaceId=` and no token - a dialer checking before it places a call. The workspace must subscribe to your application either way.

`runs` answers the question you actually have - may this workspace run service or sales calls, dialled by a rep (`preview`) or by software (`automatic`) - evaluated by the same gates the dialer refuses on, with the first `reason` worded for the member. Do not re-derive it from `steps`.

Send the member to `setupUrl` to finish a step; it carries your `client_id` so Connect offers the way back. `canManage` is their `channel:manage` - whether they can take the steps or need an admin - and always `false` on a server call, which has no member.

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | query | no | Required without a session token; ignored with one |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | The journey's state |
| `400` | validation_error - no session token and no workspaceId |
| `403` | forbidden - key missing calls:place, the workspace does not subscribe to your application, or the session token is not valid for it |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `intent` | `any` | yes |  |
| `progress` | `object` | yes |  |
| `steps` | `array` | yes |  |
| `runs` | `array` | yes |  |
| `setupUrl` | `string` | yes |  |
| `canManage` | `boolean` | yes |  |

### POST `/api/v1/application/calls/browser-token`

Sign a member's browser in to the phone network

**Scope** `calls:place` &nbsp;·&nbsp; **Rate limit** 30 requests / min per IP, 30 requests / min per API key

A short-lived carrier access token for Plivo's Browser SDK, which a member's browser logs in with before placing a call. **Member-only:** requires `X-Connect-Session-Token`, and that member must hold "Make calls" (`call:place`) - from their workspace role, or a role on your application carrying `connect:call:place`.

The token can place no call on its own. Every call it starts is refused by Connect unless `POST /calls/start` approved it first, so it carries no authority beyond signing in. It is valid for an hour, and the SDK stays registered between calls - a member making calls all morning signs in a handful of times.

You do not need to call this yourself: the SDK's `<ConnectCallButton>` does, through your bridge.

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | The token and when it expires |
| `403` | forbidden - key missing calls:place, no session token, or the member lacks Make calls |
| `412` | precondition_failed - browser calling is not set up on this deployment |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `token` | `string` | yes |  |
| `expiresAt` | `string` | yes | ISO-8601 |

### POST `/api/v1/application/calls/start`

Approve a call from a member to a number

**Scope** `calls:place` &nbsp;·&nbsp; **Rate limit** 30 requests / min per IP, 30 requests / min per API key

Records a call from the signed-in member to `phone` and returns what their browser needs to place it - the number, the workspace number the callee will see, and two SIP headers to send with the call. **Member-only**, exactly as `/calls/browser-token`.

The call leaves from the **workspace's own number**, never the member's, and is **not recorded**. Every rule is applied here, before anything reaches the carrier: the workspace must hold an active number and be allowed to place calls, and the member must not already be on one (`412`).

The headers carry a one-time key valid for two minutes, and Connect dials the number on the record rather than anything the browser later sends - so a tampered browser can ring nobody it was not approved to. Read the outcome afterwards with `GET /calls/{callId}` (`calls:read`): `handler` is `member`, `talkSeconds` is how long the callee was on the line, and `unansweredReason` says why nobody picked up. Your application can read a call it placed even though the number belongs to the workspace.

**Request body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `phone` | `string` | yes |  |
| `purpose` | `service` \| `transactional` \| `promotional` | no |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `201` | The approved call |
| `400` | validation_error - phone is not E.164 |
| `403` | forbidden - key missing calls:place, no session token, or the member lacks Make calls |
| `409` | do_not_call - the number is on the workspace's do-not-call list (see GET /do-not-call); conflict - no active number, a blocked workspace, or the member is already on a call |
| `412` | precondition_failed - no active number, a blocked workspace, or the member is already on a call |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `callId` | `string` | yes |  |
| `toNumber` | `string` | yes |  |
| `fromNumber` | `string` | yes |  |
| `headers` | `object` | yes |  |

### POST `/api/v1/application/calls/cancel`

Cancel a call the browser never placed

**Scope** `calls:place` &nbsp;·&nbsp; **Rate limit** 30 requests / min per IP, 30 requests / min per API key

For a call `/calls/start` approved but the browser could not place - the microphone was refused, sign-in failed, the member pressed Hang up while it was still connecting. The call is recorded as `unanswered` with `unansweredReason: "cancelled"`.

Only the member who started it may cancel it, and only before the carrier has taken it; after that the carrier's own report closes the record, and this does nothing. Someone else's call answers `404`.

**Request body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `callId` | `string` | yes |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `204` | Cancelled, or already in the carrier's hands |
| `403` | forbidden - key missing calls:place, no session token, or the member lacks Make calls |
| `404` | not_found - no such call, or not this member's |

### GET `/api/v1/application/do-not-call`

Read whether a number asked the workspace not to call it

**Scope** `do-not-call:read` &nbsp;·&nbsp; **Rate limit** 120 requests / min per API key

The workspace's do-not-call list is Connect's, and Connect enforces it: every outbound call - a member's `/calls/start`, from any application or from Connect itself, and every call an agent places - to a listed number is refused with `do_not_call` (409) before anything reaches the carrier. Read this to *show* the entry beside a record, never to decide whether to dial.

A number never listed answers `doNotCall: false` with every other field `null`, not `404`. `?history=true` adds the last twenty set/lifted events, newest first. Server-to-server, name the workspace with `workspaceId`; with `X-Connect-Session-Token` the workspace comes from the token and the member needs `contact:read`.

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | query | no |  |
| `phone` | query | yes |  |
| `history` | query | no |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | The number's standing on the list |
| `400` | validation_error - phone missing or not E.164 |
| `403` | forbidden - key missing do-not-call:read, no subscription in the workspace, or the member lacks contact:read |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `status` | `object` | yes |  |
| `history` | `any` | yes |  |

### PUT `/api/v1/application/do-not-call`

Put a number on the workspace's do-not-call list

**Scope** `do-not-call:write` &nbsp;·&nbsp; **Rate limit** 120 requests / min per API key

Records that a person asked this workspace not to call them. From then on Connect refuses every outbound call to the number, whichever application or agent places it. Every purpose - a person who said "stop ringing me" meant the service call too.

**Idempotent:** a number already listed keeps its original entry - when they *first* asked is the fact worth keeping - and answers `changed: false`, so a backfill can be re-run. Every change is written to an append-only ledger attributed to your application, and to the member when `X-Connect-Session-Token` came with it (they need `call:place` - whoever can ring a person can record that they asked to stop). Server-to-server, `actor` names who made the change in your own terms; it is kept as your claim, never as a Connect user.

**Request body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | `string` | no |  |
| `phone` | `string` | yes |  |
| `reason` | `asked_on_call` \| `asked_in_writing` \| `complaint` \| `imported` \| `other` | yes |  |
| `note` | `any` | no |  |
| `actor` | `object` | no |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | The entry as it now stands |
| `400` | validation_error - phone not E.164, or an unknown reason |
| `403` | forbidden - key missing do-not-call:write, no subscription in the workspace, or the member lacks call:place |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `changed` | `boolean` | yes |  |
| `status` | `object` | yes |  |

### POST `/api/v1/application/do-not-call/clear`

Lift a number's do-not-call entry

**Scope** `do-not-call:write` &nbsp;·&nbsp; **Rate limit** 120 requests / min per API key

Takes a number off the list, so calls to it are placed again. `note` is required: lifting the entry is a claim that the person took their request back, and the ledger keeps who made it and why. The entry is kept, lifted, with its history; a number not listed answers `changed: false`. With a session token the member needs `contact:manage`.

**Request body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | `string` | no |  |
| `phone` | `string` | yes |  |
| `note` | `string` | yes |  |
| `actor` | `object` | no |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | The entry as it now stands |
| `400` | validation_error - phone not E.164, or no note |
| `403` | forbidden - key missing do-not-call:write, no subscription in the workspace, or the member lacks contact:manage |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `changed` | `boolean` | yes |  |
| `status` | `object` | yes |  |