Sessions
A session is a room where several people record at the same time, each on their own device. Use it when you need a conversation; use a capture request when you need one person answering on their own time.
Open a room
curl -X POST https://capturly.app/api/v1/sessions \
-H "Authorization: Bearer cap_live_..."{
"session": { "id": "ses_...", "roomCode": "ABC123", "status": "open", "maxGuests": 7 },
"joinUrl": "https://capturly.app/record/join/ABC123?invite=…",
"hostUrl": "https://capturly.app/record/host/ABC123?invite=…",
"inviteToken": "…",
"hostToken": "…"
}joinUrl is for guests, who need no account — the invite token in it is the
whole credential. hostUrl is for the host, who does need one: the invite
admits guests, and the host is recognised by their signed-in session rather
than by the link.
The tokens are returned once and expire in 24 hours, matching the session’s own cutoff for issuing them.
The host of record is the member who created the API key. A room needs an
accountable person, for the plan its guests record under and for whoever can
end it, and a key is not one. If that member leaves the organization, session
creation fails with key_owner_not_a_member rather than silently recording
against their personal account.
Status
Three states, not the two the underlying row stores:
| Status | Meaning |
|---|---|
open | Accepting joins |
closed | Ended by the host or the API |
expired | Never closed, but past its 24-hour cutoff |
An open session past its cutoff accepts nobody. Reporting it as open would
let you hand out a join link that silently fails, so it reports expired.
End it
curl -X POST https://capturly.app/api/v1/sessions/{id}/end \
-H "Authorization: Bearer cap_live_..."This closes the room and promotes whatever was uploaded into a library
recording. It is idempotent, and ending an already-closed session re-attempts
promotion rather than refusing — uploads can land after a close, and a first
promotion can fail mid-copy. That makes it the recovery call as well as the
close call, which is why it answers 200 with alreadyClosed rather than a
conflict.
promotion says what happened, because recordingId: null alone cannot tell
“nobody recorded” from “the copy failed and the tracks are still waiting”:
promotion | Meaning |
|---|---|
created | The tracks were promoted. savedCount is how many files landed. |
existing | Already promoted by an earlier call. savedCount is 0. |
empty | Nobody recorded. There is nothing to collect. |
failed | The copy did not finish. The tracks are still held; call this again. |
A promotion failure does not fail the request: your intent was to close the
room, the cleanup job retries, and a 500 here would invite a retry loop that
closes nothing new. failed carries an errorId to quote to support, and
calling end again is the retry — it is idempotent, so a later success comes
back as created or existing.
Endpoints
| Method | Path |
|---|---|
GET | /v1/sessions |
POST | /v1/sessions |
GET | /v1/sessions/{id} |
POST | /v1/sessions/{id}/end |
Sessions are keyed on their id, not the room code. The code is a short,
human-typed handle; the id is what /v1/recordings reports as sessionId, so
the two surfaces join on the same value.
Participant identity stays inside. You get a stable per-session participantId
and whether someone was a guest, not their user id or the credential they
joined with.