Calls and channels
The phone numbers assigned to your application, and the calls that arrived on them.
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.
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.
Channels are claimed, never chosen
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 409s, 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.
destinationis 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.
setRoutingthrowsconflictwhen the channel is suspended or released.clearRoutingis still allowed there, deliberately — it is the only way to say stop answering. Overwritingdestinationcan 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
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.
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.
Next steps
- Webhooks —
call.completedandcall.missedcarry thecallIdcalls.gettakes, plus achannelIdthat may benulland acontactPhonein the same canonical formcontacts.lookupByPhonewants. - API reference — the endpoints, their schemas and the scope each needs.