Call history
Every call and room leaves a record: who started it, who was rung, what became of each person, how it ended and how long it was connected. This page is for reading that history. To place and answer calls, see Calls.
A record is written when the call starts, with status: "live", and
completed when it ends. While it is live, answered_at, ended_at,
end_reason and duration_ms are null, and everyone rung shows as
invited. The final outcome of each person is written when the call
ends.
The same moments fire the call.started, call.answered and
call.finished webhooks.
The call record
{
"id": "4a6922a7-0c3e-4b1d-9f5a-8e2c6d0b7a14",
"conversation_id": "c8a1f4e2-6b3d-4f7a-9c2e-1d5b8a0f3e61",
"kind": "call",
"media": "video",
"status": "ended",
"started_at": "2026-10-26T07:33:20.000000Z",
"answered_at": "2026-10-26T07:33:25.000000Z",
"ended_at": "2026-10-26T07:34:25.000000Z",
"end_reason": "finished",
"duration_ms": 60000,
"started_by": { "id": "11111111-2222-3333-4444-555555555555", "external_id": "acme-alice" },
"participants": [
{ "id": "22222222-3333-4444-5555-666666666666", "external_id": "acme-bob", "status": "joined" },
{ "id": "33333333-4444-5555-6666-777777777777", "external_id": "acme-carol", "status": "declined" }
]
}
| Field | Type | Description |
|---|---|---|
id | uuid | The call's id, the same one the Calls endpoints use. |
conversation_id | uuid | |
kind | "call" | "room" | A call rings the other members. A room rings nobody: people drop in. |
media | "audio" | "video" | |
status | "live" | "ended" | |
started_at | string | ISO 8601. |
answered_at | string | null | When the first person rung picked up. null for rooms, unanswered calls, and while live. |
ended_at | string | null | |
end_reason | string | null | See below. null while live. |
duration_ms | integer | null | Connected time. For a call, from the first answer to the end (null when nobody answered). For a room, from start to end. null while live. |
started_by | object | null | { id, external_id } of whoever placed the call or opened the room. |
participants | object[] | { id, external_id, status } for everyone else rung or present. The starter is not included. |
end_reason values:
| Value | Meaning |
|---|---|
finished | The call or room ran and everyone left. |
cancelled | The caller hung up before anyone answered. |
no_answer | The ring ran out with nobody answering. |
declined_all | Everyone rung declined. |
busy_all | Everyone rung was busy on another call. |
Participant status values: joined, declined, busy (their
phone was on another call), missed (rung, never answered), and while
the call is live, invited.
List the tenant's calls
/v1/callsFor your backend: every call and room in the tenant, newest first. Any valid API key works; no scope is needed.
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
conversation_id | uuid | none | Only this conversation's calls. |
external_id | string | none | Only calls this user started or was part of. An unknown external_id returns an empty list. |
kind | call | room | none | |
status | live | ended | none | |
since | ISO 8601 time | none | Started at or after this time. Include a UTC offset, e.g. 2026-10-01T00:00:00Z. |
until | ISO 8601 time | none | Started before this time. |
limit | integer | 50 | 1–200. Values outside the range are clamped. |
cursor | string | none | The previous page's next_cursor. |
Response
200:
{ "data": [ CallRecord, … ], "next_cursor": string | null }.
next_cursor is null on the last page. Treat it as an opaque string
and pass it back unchanged as cursor.
curl "https://api.poolse.dev/v1/calls?external_id=acme-bob&status=ended&limit=100" \
-H "Authorization: Bearer $POOLSE_API_KEY"
Errors
| Code | Status | When |
|---|---|---|
invalid_parameter | 400 | conversation_id is not a UUID, or since / until is not an ISO 8601 time. |
invalid_cursor | 400 | cursor is not one the server issued. |
Get a call
/v1/calls/:id200: { "data": CallRecord }. Note the data wrapper.
| Code | Status | When |
|---|---|---|
not_found | 404 | No such call in the tenant. |
A user's own calls
/v1/me/callsFor your app, with the signed-in user's token: every call they started or were part of, newest first. This is what a calls tab lists. See Building a calls tab.
Query parameters: kind, status, since, until, limit, cursor,
as above. conversation_id and external_id are not accepted here.
200: { "data": [ CallRecord, … ], "next_cursor": … }.
Errors: invalid_parameter and invalid_cursor, as above.
A conversation's calls
/v1/conversations/:conversation_id/callsEvery call and room on one conversation, newest first. Only for members of the conversation.
Query parameters: kind, status, since, until, limit, cursor,
as above.
200: { "data": [ CallRecord, … ], "next_cursor": … }.
| Code | Status | When |
|---|---|---|
not_found | 404 | No such conversation, or the caller is not a member. |
invalid_parameter | 400 | since / until is not an ISO 8601 time. |
invalid_cursor | 400 | Bad cursor. |
In the conversation itself
When a call or room ends, poolse also posts one message of
type: "system" into the conversation. Its body is a JSON string with
kind: "call", v: 2, call_kind (call or room), call_id,
media, caller_user_id, started_at, ended_at, end_reason,
duration_ms, joined_user_ids and participants ({ user_id, status }).
Clients render it as a call bubble. See Messages.