Two reads over the same engagement:

| Resource | What it holds | Owned by | Scope |
| --- | --- | --- | --- |
| `connect.channels` | The numbers assigned to your application | the platform | `channels:read` / `channels:claim` / `channels:manage` |
| `connect.calls` | Every call that arrived on one of them | your application | `calls:read` |

A call arrived on a channel *you* hold, so `calls.list` is scoped to your
application.

<Callout title="WhatsApp conversations live in Messaging">
Connect no longer holds customer WhatsApp threads, and `connect.conversations`
has been removed. A workspace's own WhatsApp number, its inbox and the agent
answering on it belong to the CosmoOps Messaging application. Connect still
sends its own templates, OTPs and notifications from the platform number - see
[WhatsApp](/docs/api/whatsapp).
</Callout>

## Channels are claimed, never chosen

```ts
const channel = await connect.channels.claim({ workspaceId });
await connect.channels.setRouting(channel.channelId, { kind: "agent", agentId });
```

**Inventory is platform-owned.** A number is a regulated asset bought under
Connect's carrier account and reassignable between workspaces, so your
application never provisions, prices or releases one — it claims from a pool
it cannot see and reads back what it was given. That is why `claim` takes a
workspace and a kind rather than a number, and why nothing here exposes a
carrier, a provider reference or a monthly rate.

A workspace may hold **exactly one** number, across every application it
uses, and it is then fixed for the life of the engagement: there is no
re-claim and no swap, and only a Connect operator can unassign one.

`kind` defaults to `"voice"`, the only kind Connect stocks inventory for
today.

**Two different `409`s, needing different handling:** the workspace already
holds a channel of this kind — fix the call — or the pool is empty right now,
which is worth retrying, because Connect is notified and it is not permanent.

### Routing, and how to stop answering

A claimed channel comes back **without routing**, because it has none yet.
Call `setRouting` next, or the number refuses every call that reaches it.

- `destination` is yours. Connect stores it verbatim and never reads into it.
- It **replaces** the stored record rather than merging, so send the whole
  destination each time. It must serialize to 16 KB or fewer.
- `setRouting` throws `conflict` when the channel is suspended or released.
  `clearRouting` is still allowed there, deliberately — it is the only way to
  say *stop answering*. Overwriting `destination` can only ever point the
  number somewhere else, and unassigning it is a Console action that returns
  it to Connect's inventory. A cleared channel stays assigned to you and stays
  billable.

## Calls

```ts
const page = await connect.calls.list({ channelId });
const call = await connect.calls.get(callId);
```

The application is resolved from your API key and is **never a parameter**, so
there is no request shape that reads another partner's calls.

`channelId` is the only filter, and that is a budget decision rather than an
oversight: each filter costs a Firestore composite index against a project
already near its ceiling. Filter the rest client-side, or read the aggregates
from `connect.analytics`.

`get` returns every transcript turn ordered by **`sequence`** — the sort to
trust, since two turns can share a millisecond.
`recordingFileId` and `transcriptFileId` are Drive file ids readable with a
`drive:read` key, and both are `null` until the carrier posts the audio. That
is the normal state for a call that has only just ended, so branch on
`recordingStatus` rather than treating `null` as a failure.

<Callout type="warn" title="Page until hasMore is false">
Do not stop on an empty `items`. A page can come back empty with more behind
it — on calls and on channels alike. Neither list carries a total, because a
call log grows without bound.
</Callout>

## Next steps

- **[Webhooks](/docs/webhooks)** — `call.completed` and `call.missed` carry the
  `callId` `calls.get` takes, plus a `channelId` that may be `null` and a
  `contactPhone` in the same canonical form `contacts.lookupByPhone` wants.
- **[API reference](/docs/api/channels)** — the endpoints, their schemas and
  the scope each needs.