Your backend calls these. Tenant comes from the key — a tenant_id in a body is rejected. Scope-gated.
23 operations · 23 captured against the live API.
Rooms & members #
Provision a room (idempotent on external_ref)
provisionRoom · scope rooms:write captured live
Request
curl -X POST https://api.ollacore.com/v1/server/rooms \
-H "Authorization: Bearer ak_live_…" \
-H "Content-Type: application/json" \
-d '{"created_by":"alice","external_ref":"order-7781"}'
{
"created_by": "alice",
"external_ref": "order-7781"
}
| Parameter | Type | Description |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
created_by required | string | The principal recorded as the room's creator, 1–128 characters. It does not make them a member; add members separately. |
external_ref | string | Your own reference for the room. If a room with this external_ref already exists in the tenant, it is returned instead of a new one. |
expires_at | string (date-time) | Optional informational expiry. Stored and returned; nothing closes or deletes the room when it passes. |
e2ee | boolean | Create an end-to-end encrypted room. The server then relays only ciphertext (mls.* kinds) and refuses search, reactions and previews. Cannot be changed later. |
e2ee_scheme | one of: mls, megolm | Declare which client scheme the members will run: mls (primary) or megolm. Requires e2ee: true (400 otherwise); immutable; echoed on the room so clients load the right engine. |
Response · 200
{
"id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
"tenant_id": "acct_fac91b282191cc02dd29912c8db43f1c",
"external_ref": "order-7781",
"created_by": "alice",
"status": "active",
"e2ee": false,
"created_at": "2026-01-15T09:30:03Z"
}
Response fields (9)
| Field | Type | Description |
id required | string (uuid) | The room's id, assigned by the server. |
tenant_id required | string | The tenant that owns the room. |
external_ref | string | Your own reference for the room, if you gave one. Creating a room with an external_ref that already exists in the tenant returns that room. |
created_by required | string | The principal recorded as the room's creator. |
status required | one of: active, closed | active, or closed (no new tokens are minted for it). |
e2ee required | boolean | End-to-end encrypted room. When true, the server accepts only ciphertext message kinds, rejects search and reactions, and pushes are content-free — the client must encrypt before sending. |
e2ee_scheme | one of: mls, megolm | Which client E2EE scheme the room's members run, when the creator declared it: mls (the primary scheme) or megolm (the matrix-sdk-crypto transport). The two cannot share a room. Absent when undeclared and on plaintext rooms. |
created_at required | string (date-time) | When the room was created. |
expires_at | string (date-time) | Informational expiry time, if one was set. Nothing closes or deletes the room when it passes. |
Errors · 401 · 403 · 422 · 429
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
422 | Unknown field (e.g. a supplied tenant_id) rejected |
429 | Rate limit exceeded |
GET /v1/server/rooms/{room_id} # Fetch a room
getRoom · scope rooms:read captured live
Request
curl -X GET https://api.ollacore.com/v1/server/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1 \
-H "Authorization: Bearer ak_live_…"
| Parameter | Type | Description |
room_id path required | string (uuid) | The room's id. A room in another tenant, or one being erased, is 404. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
"tenant_id": "acct_fac91b282191cc02dd29912c8db43f1c",
"external_ref": "order-7781",
"created_by": "alice",
"status": "active",
"e2ee": false,
"created_at": "2026-01-15T09:30:03Z"
}
Response fields (9)
| Field | Type | Description |
id required | string (uuid) | The room's id, assigned by the server. |
tenant_id required | string | The tenant that owns the room. |
external_ref | string | Your own reference for the room, if you gave one. Creating a room with an external_ref that already exists in the tenant returns that room. |
created_by required | string | The principal recorded as the room's creator. |
status required | one of: active, closed | active, or closed (no new tokens are minted for it). |
e2ee required | boolean | End-to-end encrypted room. When true, the server accepts only ciphertext message kinds, rejects search and reactions, and pushes are content-free — the client must encrypt before sending. |
e2ee_scheme | one of: mls, megolm | Which client E2EE scheme the room's members run, when the creator declared it: mls (the primary scheme) or megolm (the matrix-sdk-crypto transport). The two cannot share a room. Absent when undeclared and on plaintext rooms. |
created_at required | string (date-time) | When the room was created. |
expires_at | string (date-time) | Informational expiry time, if one was set. Nothing closes or deletes the room when it passes. |
Errors · 401 · 403 · 404
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
DELETE /v1/server/rooms/{room_id} # Erase a room — rows and blobs
deleteRoom · scope rooms:delete captured live
Request
curl -X DELETE https://api.ollacore.com/v1/server/rooms/0e56d494-4ac9-490d-9b2c-58b50ab02ac0 \
-H "Authorization: Bearer ak_live_…"
| Parameter | Type | Description |
room_id path required | string (uuid) | The room's id. A room in another tenant, or one being erased, is 404. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"deleting": true
}
Response fields (1)
| Field | Type | Description |
deleting required | boolean | True: the room is inaccessible now, and its rows and files are erased in the background. |
Errors · 401 · 403 · 404
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
POST /v1/server/rooms/{room_id}/members # Add or restore a member (409 once the room holds 5,000)
addRoomMember · scope members:write captured live
Request
curl -X POST https://api.ollacore.com/v1/server/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/members \
-H "Authorization: Bearer ak_live_…" \
-H "Content-Type: application/json" \
-d '{"principal_id":"alice","role":"owner"}'
{
"principal_id": "alice",
"role": "owner"
}
| Parameter | Type | Description |
room_id path required | string (uuid) | The room's id. A room in another tenant, or one being erased, is 404. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
principal_id required | string | Who to add, 1–128 characters. Any opaque id you choose; a removed member is restored. |
role | one of: owner, member, viewer | owner, member or viewer. Anything else is 400. |
Response · 200
{
"tenant_id": "acct_fac91b282191cc02dd29912c8db43f1c",
"room_id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
"principal_id": "alice",
"role": "owner",
"joined_at": "2026-01-15T09:30:03Z"
}
Response fields (7)
| Field | Type | Description |
tenant_id required | string | The tenant that owns the room. |
room_id required | string (uuid) | The room. |
principal_id required | string | The member's principal id. |
role required | string | owner, member or viewer. |
joined_at | string (date-time) | When the principal first joined the room. Restoring a removed member keeps the original time. |
left_at | string (date-time) | set when the member was removed; the row is retained so history stays auditable |
muted_until | string (date-time) | Until when this member has push notifications from the room silenced; absent when not muted. 9999-12-31T00:00:00Z stands for "until unmuted". Live delivery and call rings are unaffected. |
Errors · 401 · 403 · 404 · 409
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
409 | Conflicts with current state |
DELETE /v1/server/rooms/{room_id}/members/{principal_id} # Remove a member
removeRoomMember · scope members:write captured live
Request
curl -X DELETE https://api.ollacore.com/v1/server/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/members/carol \
-H "Authorization: Bearer ak_live_…"
| Parameter | Type | Description |
room_id path required | string (uuid) | The room's id. A room in another tenant, or one being erased, is 404. |
principal_id path required | string | The member to remove. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"removed": true
}
Response fields (1)
| Field | Type | Description |
removed | boolean | True if the principal was an active member and is now removed. Their open sockets close within 30 seconds. |
Errors · 401 · 403 · 404
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
PUT /v1/server/rooms/{room_id}/members/{principal_id}/mute # Silence the room's push for a member — 8h, 1w or always (members:write)
muteRoomMember · scope members:write captured live
Request
curl -X PUT https://api.ollacore.com/v1/server/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/members/bob/mute \
-H "Authorization: Bearer ak_live_…" \
-H "Content-Type: application/json" \
-d '{"duration":"8h"}'
{
"duration": "8h"
}
| Parameter | Type | Description |
room_id path required | string (uuid) | The room's id. A room in another tenant, or one being erased, is 404. |
principal_id path required | string | The member. 404 if they are not an active member of the room. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
duration required | one of: 8h, 1w, always | How long to silence push notifications: eight hours, one week, or until unmuted. 9999-12-31T00:00:00Z stands for "until unmuted". |
Response · 200
{
"tenant_id": "acct_fac91b282191cc02dd29912c8db43f1c",
"room_id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
"principal_id": "bob",
"role": "member",
"joined_at": "2026-01-15T09:30:03Z",
"muted_until": "2026-01-15T17:30:03Z"
}
Response fields (7)
| Field | Type | Description |
tenant_id required | string | The tenant that owns the room. |
room_id required | string (uuid) | The room. |
principal_id required | string | The member's principal id. |
role required | string | owner, member or viewer. |
joined_at | string (date-time) | When the principal first joined the room. Restoring a removed member keeps the original time. |
left_at | string (date-time) | set when the member was removed; the row is retained so history stays auditable |
muted_until | string (date-time) | Until when this member has push notifications from the room silenced; absent when not muted. 9999-12-31T00:00:00Z stands for "until unmuted". Live delivery and call rings are unaffected. |
Errors · 401 · 403 · 404 · 422
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
422 | A duration outside 8h|1w|always, or an unknown field. |
Push only: the member still receives every event live and a call still rings. Durations are 8h, 1w or always; for always, muted_until is a date in the year 9999.
DELETE /v1/server/rooms/{room_id}/members/{principal_id}/mute # Unmute (idempotent)
unmuteRoomMember · scope members:write captured live
Request
curl -X DELETE https://api.ollacore.com/v1/server/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/members/bob/mute \
-H "Authorization: Bearer ak_live_…"
| Parameter | Type | Description |
room_id path required | string (uuid) | The room's id. A room in another tenant, or one being erased, is 404. |
principal_id path required | string | The member. 404 if they are not an active member of the room. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"tenant_id": "acct_fac91b282191cc02dd29912c8db43f1c",
"room_id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
"principal_id": "bob",
"role": "member",
"joined_at": "2026-01-15T09:30:03Z"
}
Response fields (7)
| Field | Type | Description |
tenant_id required | string | The tenant that owns the room. |
room_id required | string (uuid) | The room. |
principal_id required | string | The member's principal id. |
role required | string | owner, member or viewer. |
joined_at | string (date-time) | When the principal first joined the room. Restoring a removed member keeps the original time. |
left_at | string (date-time) | set when the member was removed; the row is retained so history stays auditable |
muted_until | string (date-time) | Until when this member has push notifications from the room silenced; absent when not muted. 9999-12-31T00:00:00Z stands for "until unmuted". Live delivery and call rings are unaffected. |
Errors · 401 · 403 · 404
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
PUT /v1/server/rooms/{room_id}/members/{principal_id}/preferences # Pin which simulcast layer of a publisher's track the member receives — low, medium, high or auto (members:write)
setMemberPreferences · scope members:write captured live
Request
curl -X PUT https://api.ollacore.com/v1/server/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/members/bob/preferences \
-H "Authorization: Bearer ak_live_…" \
-H "Content-Type: application/json" \
-d '{"tracks":[{"publisher":"alice","track_id":"cam-alice","layer":"high"}]}'
{
"tracks": [
{
"publisher": "alice",
"track_id": "cam-alice",
"layer": "high"
}
]
}
| Parameter | Type | Description |
room_id path required | string (uuid) | The room's id. A room in another tenant, or one being erased, is 404. |
principal_id path required | string | The member whose received layers are being chosen. 404 if they are not an active member of the room. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
tracks required | array of TrackPreference max 128 items | The member's whole list, replaced. An auto entry clears that track; an empty list clears everything. |
tracks[].publisher required | string min length 1 · max length 128 | The publisher's principal id — the msid stream id a chatbox.msid-principal socket sees. |
tracks[].track_id required | string min length 1 · max length 256 | The publisher's own track id as the msid carries it (or the <client id>-<mid> stand-in a bare m-line gets). |
tracks[].layer required | one of: low, medium, high, auto | Named by quality, resolved against the layers the publisher is sending now, ordered by measured bitrate: low the cheapest, high the dearest, medium the middle one or the dearer of two. auto is no preference: the adaptive policy decides. |
Response · 200
{
"principal_id": "bob",
"in_call": false,
"tracks": [
{
"publisher": "alice",
"track_id": "cam-alice",
"layer": "high"
}
]
}
Response fields (6)
| Field | Type | Description |
principal_id required | string | |
in_call required | boolean | Whether the member has a signaling connection in this room's call right now. Preferences set for a member who is not — or whose last connection just closed — are held for a 30 s grace and then dropped. |
tracks required | array of TrackPreference | What is pinned, sorted by publisher then track id. auto entries are never listed. |
tracks[].publisher required | string min length 1 · max length 128 | The publisher's principal id — the msid stream id a chatbox.msid-principal socket sees. |
tracks[].track_id required | string min length 1 · max length 256 | The publisher's own track id as the msid carries it (or the <client id>-<mid> stand-in a bare m-line gets). |
tracks[].layer required | one of: low, medium, high, auto | Named by quality, resolved against the layers the publisher is sending now, ordered by measured bitrate: low the cheapest, high the dearest, medium the middle one or the dearer of two. auto is no preference: the adaptive policy decides. |
Errors · 400 · 401 · 403 · 404 · 422
| Status | When |
400 | More than 128 entries, a publisher outside 1..=128 characters, a track_id outside 1..=256 characters, or the same publisher and track listed twice. |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
422 | A layer outside low|medium|high|auto, a missing field, or an unknown field. |
Keyed by the publisher's principal and the publisher's own track id — what a chatbox.msid-principal socket reads off the msid. The whole list is replaced on each PUT; an auto entry clears that track, [] clears all. Applied within about a second to every connection the member has; a member not in the call keeps it for a 30 s grace.
GET /v1/server/rooms/{room_id}/members/{principal_id}/preferences # What is pinned for the member, and whether they are in the call (rooms:read)
getMemberPreferences · scope rooms:read captured live
Request
curl -X GET https://api.ollacore.com/v1/server/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/members/bob/preferences \
-H "Authorization: Bearer ak_live_…"
| Parameter | Type | Description |
room_id path required | string (uuid) | The room's id. A room in another tenant, or one being erased, is 404. |
principal_id path required | string | The member whose received layers are being chosen. 404 if they are not an active member of the room. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"principal_id": "bob",
"in_call": false,
"tracks": [
{
"publisher": "alice",
"track_id": "cam-alice",
"layer": "high"
}
]
}
Response fields (6)
| Field | Type | Description |
principal_id required | string | |
in_call required | boolean | Whether the member has a signaling connection in this room's call right now. Preferences set for a member who is not — or whose last connection just closed — are held for a 30 s grace and then dropped. |
tracks required | array of TrackPreference | What is pinned, sorted by publisher then track id. auto entries are never listed. |
tracks[].publisher required | string min length 1 · max length 128 | The publisher's principal id — the msid stream id a chatbox.msid-principal socket sees. |
tracks[].track_id required | string min length 1 · max length 256 | The publisher's own track id as the msid carries it (or the <client id>-<mid> stand-in a bare m-line gets). |
tracks[].layer required | one of: low, medium, high, auto | Named by quality, resolved against the layers the publisher is sending now, ordered by measured bitrate: low the cheapest, high the dearest, medium the middle one or the dearer of two. auto is no preference: the adaptive policy decides. |
Errors · 401 · 403 · 404
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
PUT /v1/server/rooms/{room_id}/members/{principal_id}/rtc-policy # Restrict what the member may publish and receive, which peers it sees, how many devices it may bring, and its chat and attachment rights — revisioned, live within about a second (members:write)
setMemberRtcPolicy · scope members:write captured live
Request
curl -X PUT https://api.ollacore.com/v1/server/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/members/alice/rtc-policy \
-H "Authorization: Bearer ak_live_…" \
-H "Content-Type: application/json" \
-d '{"revision":3,"publish":{"audio":false,"video":true,"max_audio_tracks":0,"max_video_tracks":1},"subscribe":{"mode":"allowlist","publishers":["host"]},"chat":{"send":false,"history":false},"attachments":{"upload":false,"download":false}}'
{
"revision": 3,
"publish": {
"audio": false,
"video": true,
"max_audio_tracks": 0,
"max_video_tracks": 1
},
"subscribe": {
"mode": "allowlist",
"publishers": [
"host"
]
},
"chat": {
"send": false,
"history": false
},
"attachments": {
"upload": false,
"download": false
}
}
| Parameter | Type | Description |
room_id path required | string (uuid) | The room's id. A room in another tenant, or one being erased, is 404. |
principal_id path required | string | The member the policy applies to. 404 if they are not an active member of the room. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
revision required | integer (int64) min 1 | Your monotonic revision for this member's policy. Higher than stored: replaced. Equal with the same document: no-op. Equal with a different document: 409 revision_conflict. Lower: 409 stale_revision. Restarts after a DELETE. |
publish | RtcPublishPolicy | |
publish.audio | boolean default true | May publish audio. |
publish.video | boolean default true | May publish video (any source — the server cannot tell a screen from a camera). |
publish.max_audio_tracks | integer min 0 | Most audio publications active at once. Omitted: no limit. |
publish.max_video_tracks | integer min 0 | Most video publications active at once. Omitted: no limit. |
subscribe | RtcSubscribePolicy | |
subscribe.mode required if its parent is sent | one of: all, none, allowlist | Which publishers the member may receive. Applied by the media loop from the leg's first offer: a refused publisher's sections are never offered; a tightening stops its packets at once, removes its sections and re-offers the leg; a loosening offers the newly allowed publishers. |
subscribe.publishers | array of string max 64 items | Principal ids, exact opaque strings, 1–128 characters each, at most 64; only with allowlist. A SET: duplicates are 400, the server sorts the ids canonically before storing and comparing, order never affects forwarding, and the same set in a different order at an equal revision is an idempotent no-op, not revision_conflict. GET returns the sorted list. |
chat | RtcChatPolicy | |
chat.send | boolean default true | May send chat messages. false: a send fails with an error ack chat_send_forbidden; the socket stays open. |
chat.history | boolean default true | May read history. false also closes the chat socket with reason policy_changed. |
attachments | RtcAttachmentPolicy | |
attachments.upload | boolean default true | |
attachments.download | boolean default true | |
roster | one of: all, authorized-only, none | Who a restricted subscriber sees in the RTC roster frames (peers, peer.joined, peer.left): all, authorized-only (the publishers it may receive, nobody else) or none. Omitted: all unless subscribe.mode is none or allowlist, then authorized-only. A change re-sends the member a fresh peers snapshot under the new filter. |
devices | RtcDevicePolicy | How many devices the member may have in the call at once. A device is the session token's device_id: under this policy a new leg with the same device_id as a live one is that device reconnecting and replaces its predecessor (closed 4412 device_reconnected) whatever on_limit says, never counting twice. A leg minted without a device_id is a device of its own — mint with one when using a limit, or a reconnect can be refused until the abandoned leg's keepalive gives up (up to 50 s). Counts RTC call legs only, not chat sockets. Checked at each leg's admission, and re-applied to live legs within about a second when the policy changes: a lowered limit closes the surplus at once (reject-new: the newest devices, 4411; replace-old: the oldest, 4412; allow: none). Every conflict is reported as an rtc.device_conflict webhook event. |
devices.max_active required if its parent is sent | integer min 1 · max 16 | Most devices the member may have in the call at once. |
devices.on_limit required if its parent is sent | one of: allow, reject-new, replace-old | allow: admit the new device anyway and only report it. reject-new: the new leg gets an error event (reason: device_limit) and is closed 4411; the devices already in the call are untouched. replace-old: the new leg is admitted and the member's oldest device is closed 4412 policy_changed. |
Response · 200
{
"principal_id": "alice",
"stored_revision": 3,
"applied_revision": null,
"applied": false,
"applied_at": null,
"in_call": false,
"active_legs": 0,
"applied_legs": 0,
"subscribe_enforced": true,
"enforcement": {
"publish": {
"supported": true,
"requested": true,
"applied_revision": null
},
"capabilities": {
"supported": true,
"requested": true,
"applied_revision": 3
},
"subscribe": {
"supported": true,
"requested": true,
"applied_revision": null
},
"roster": {
"supported": true,
"requested": false,
"applied_revision": null
},
"devices": {
"supported": true,
"requested": false,
"applied_revision": null
}
},
"policy": {
"revision": 3,
"publish": {
"audio": false,
"video": true,
"max_audio_tracks": 0,
"max_video_tracks": 1
},
"subscribe": {
"mode": "allowlist",
"publishers": [
"host"
]
},
"chat": {
"send": false,
"history": false
},
"attachments": {
"upload": false,
"download": false
}
}
}
Response fields (50)
| Field | Type | Description |
principal_id required | string | |
stored_revision required | integer (int64) | |
applied_revision required | integer (int64), nullable | The revision every active leg of the member runs; null while the member has no leg or a leg has not reported yet. |
applied required | boolean | Every component that is both supported and present in the document has reached stored_revision (see enforcement). false while the member has no live leg: there is nothing to apply the publish rules to yet. |
applied_at | string (date-time), nullable | |
in_call required | boolean | |
active_legs required | integer | |
applied_legs required | integer | Legs running applied_revision; differs from active_legs only for the moment a change is still landing. |
subscribe_enforced required | boolean | true since Phase 2: subscribe and roster are enforced. Superseded by enforcement.subscribe.supported; kept for the first clients. |
enforcement required | RtcPolicyEnforcement | Per-component enforcement state. applied at the top level is true only when every component that is both supported and requested has reached stored_revision; a requested component this release does not enforce stays visible here and is never counted as applied. |
enforcement.publish required | RtcComponentEnforcement | |
enforcement.publish.supported required | boolean | Whether this server release enforces the component at all. All four since Phase 2. |
enforcement.publish.requested required | boolean | Whether the stored document has this section (capabilities = chat or attachments). |
enforcement.publish.applied_revision required | integer (int64), nullable | The revision the component runs at. publish: the revision every live leg has applied, null with no leg. capabilities: the stored revision — read from the membership row at every request, and by a live chat socket within about a second of the write. null when unsupported or not requested. |
enforcement.capabilities required | RtcComponentEnforcement | |
enforcement.capabilities.supported required | boolean | Whether this server release enforces the component at all. All four since Phase 2. |
enforcement.capabilities.requested required | boolean | Whether the stored document has this section (capabilities = chat or attachments). |
enforcement.capabilities.applied_revision required | integer (int64), nullable | The revision the component runs at. publish: the revision every live leg has applied, null with no leg. capabilities: the stored revision — read from the membership row at every request, and by a live chat socket within about a second of the write. null when unsupported or not requested. |
enforcement.subscribe required | RtcComponentEnforcement | |
enforcement.subscribe.supported required | boolean | Whether this server release enforces the component at all. All four since Phase 2. |
enforcement.subscribe.requested required | boolean | Whether the stored document has this section (capabilities = chat or attachments). |
enforcement.subscribe.applied_revision required | integer (int64), nullable | The revision the component runs at. publish: the revision every live leg has applied, null with no leg. capabilities: the stored revision — read from the membership row at every request, and by a live chat socket within about a second of the write. null when unsupported or not requested. |
enforcement.roster required | RtcComponentEnforcement | |
enforcement.roster.supported required | boolean | Whether this server release enforces the component at all. All four since Phase 2. |
enforcement.roster.requested required | boolean | Whether the stored document has this section (capabilities = chat or attachments). |
enforcement.roster.applied_revision required | integer (int64), nullable | The revision the component runs at. publish: the revision every live leg has applied, null with no leg. capabilities: the stored revision — read from the membership row at every request, and by a live chat socket within about a second of the write. null when unsupported or not requested. |
enforcement.devices required | RtcComponentEnforcement | |
enforcement.devices.supported required | boolean | Whether this server release enforces the component at all. All four since Phase 2. |
enforcement.devices.requested required | boolean | Whether the stored document has this section (capabilities = chat or attachments). |
enforcement.devices.applied_revision required | integer (int64), nullable | The revision the component runs at. publish: the revision every live leg has applied, null with no leg. capabilities: the stored revision — read from the membership row at every request, and by a live chat socket within about a second of the write. null when unsupported or not requested. |
policy required | RtcPolicyDocument | |
policy.revision required | integer (int64) min 1 | Your monotonic revision for this member's policy. Higher than stored: replaced. Equal with the same document: no-op. Equal with a different document: 409 revision_conflict. Lower: 409 stale_revision. Restarts after a DELETE. |
policy.publish | RtcPublishPolicy | |
policy.publish.audio | boolean default true | May publish audio. |
policy.publish.video | boolean default true | May publish video (any source — the server cannot tell a screen from a camera). |
policy.publish.max_audio_tracks | integer min 0 | Most audio publications active at once. Omitted: no limit. |
policy.publish.max_video_tracks | integer min 0 | Most video publications active at once. Omitted: no limit. |
policy.subscribe | RtcSubscribePolicy | |
policy.subscribe.mode required if its parent is sent | one of: all, none, allowlist | Which publishers the member may receive. Applied by the media loop from the leg's first offer: a refused publisher's sections are never offered; a tightening stops its packets at once, removes its sections and re-offers the leg; a loosening offers the newly allowed publishers. |
policy.subscribe.publishers | array of string max 64 items | Principal ids, exact opaque strings, 1–128 characters each, at most 64; only with allowlist. A SET: duplicates are 400, the server sorts the ids canonically before storing and comparing, order never affects forwarding, and the same set in a different order at an equal revision is an idempotent no-op, not revision_conflict. GET returns the sorted list. |
policy.chat | RtcChatPolicy | |
policy.chat.send | boolean default true | May send chat messages. false: a send fails with an error ack chat_send_forbidden; the socket stays open. |
policy.chat.history | boolean default true | May read history. false also closes the chat socket with reason policy_changed. |
policy.attachments | RtcAttachmentPolicy | |
policy.attachments.upload | boolean default true | |
policy.attachments.download | boolean default true | |
policy.roster | one of: all, authorized-only, none | Who a restricted subscriber sees in the RTC roster frames (peers, peer.joined, peer.left): all, authorized-only (the publishers it may receive, nobody else) or none. Omitted: all unless subscribe.mode is none or allowlist, then authorized-only. A change re-sends the member a fresh peers snapshot under the new filter. |
policy.devices | RtcDevicePolicy | How many devices the member may have in the call at once. A device is the session token's device_id: under this policy a new leg with the same device_id as a live one is that device reconnecting and replaces its predecessor (closed 4412 device_reconnected) whatever on_limit says, never counting twice. A leg minted without a device_id is a device of its own — mint with one when using a limit, or a reconnect can be refused until the abandoned leg's keepalive gives up (up to 50 s). Counts RTC call legs only, not chat sockets. Checked at each leg's admission, and re-applied to live legs within about a second when the policy changes: a lowered limit closes the surplus at once (reject-new: the newest devices, 4411; replace-old: the oldest, 4412; allow: none). Every conflict is reported as an rtc.device_conflict webhook event. |
policy.devices.max_active required if its parent is sent | integer min 1 · max 16 | Most devices the member may have in the call at once. |
policy.devices.on_limit required if its parent is sent | one of: allow, reject-new, replace-old | allow: admit the new device anyway and only report it. reject-new: the new leg gets an error event (reason: device_limit) and is closed 4411; the devices already in the call are untouched. replace-old: the new leg is admitted and the member's oldest device is closed 4412 policy_changed. |
Errors · 400 · 401 · 403 · 404 · 409 · 429
| Status | When |
400 | revision below 1, an allowlist that is empty, longer than 64 or lists a publisher twice, publishers given with a mode other than allowlist, a publisher id outside 1..=128 characters, an unknown field, or a document over 16 KiB. |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
409 | stale_revision: the revision is lower than the stored one. revision_conflict: the same revision as stored, with a different document. The body adds current_revision. |
429 | Rate limit exceeded |
alice is not in a call during this run, so publish.applied_revision is null and applied is false: there is no leg to apply the publish rules to yet. subscribe is stored and reported, enforced from Phase 2 (enforcement.subscribe.supported: false).
GET /v1/server/rooms/{room_id}/members/{principal_id}/rtc-policy # The member's stored policy and how far it has reached their live connections, per component (rooms:read)
getMemberRtcPolicy · scope rooms:read captured live
Request
curl -X GET https://api.ollacore.com/v1/server/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/members/alice/rtc-policy \
-H "Authorization: Bearer ak_live_…"
| Parameter | Type | Description |
room_id path required | string (uuid) | The room's id. A room in another tenant, or one being erased, is 404. |
principal_id path required | string | The member the policy applies to. 404 if they are not an active member of the room. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"principal_id": "alice",
"stored_revision": 3,
"applied_revision": null,
"applied": false,
"applied_at": null,
"in_call": false,
"active_legs": 0,
"applied_legs": 0,
"subscribe_enforced": true,
"enforcement": {
"publish": {
"supported": true,
"requested": true,
"applied_revision": null
},
"capabilities": {
"supported": true,
"requested": true,
"applied_revision": 3
},
"subscribe": {
"supported": true,
"requested": true,
"applied_revision": null
},
"roster": {
"supported": true,
"requested": false,
"applied_revision": null
},
"devices": {
"supported": true,
"requested": false,
"applied_revision": null
}
},
"policy": {
"revision": 3,
"publish": {
"audio": false,
"video": true,
"max_audio_tracks": 0,
"max_video_tracks": 1
},
"subscribe": {
"mode": "allowlist",
"publishers": [
"host"
]
},
"chat": {
"send": false,
"history": false
},
"attachments": {
"upload": false,
"download": false
}
}
}
Response fields (50)
| Field | Type | Description |
principal_id required | string | |
stored_revision required | integer (int64) | |
applied_revision required | integer (int64), nullable | The revision every active leg of the member runs; null while the member has no leg or a leg has not reported yet. |
applied required | boolean | Every component that is both supported and present in the document has reached stored_revision (see enforcement). false while the member has no live leg: there is nothing to apply the publish rules to yet. |
applied_at | string (date-time), nullable | |
in_call required | boolean | |
active_legs required | integer | |
applied_legs required | integer | Legs running applied_revision; differs from active_legs only for the moment a change is still landing. |
subscribe_enforced required | boolean | true since Phase 2: subscribe and roster are enforced. Superseded by enforcement.subscribe.supported; kept for the first clients. |
enforcement required | RtcPolicyEnforcement | Per-component enforcement state. applied at the top level is true only when every component that is both supported and requested has reached stored_revision; a requested component this release does not enforce stays visible here and is never counted as applied. |
enforcement.publish required | RtcComponentEnforcement | |
enforcement.publish.supported required | boolean | Whether this server release enforces the component at all. All four since Phase 2. |
enforcement.publish.requested required | boolean | Whether the stored document has this section (capabilities = chat or attachments). |
enforcement.publish.applied_revision required | integer (int64), nullable | The revision the component runs at. publish: the revision every live leg has applied, null with no leg. capabilities: the stored revision — read from the membership row at every request, and by a live chat socket within about a second of the write. null when unsupported or not requested. |
enforcement.capabilities required | RtcComponentEnforcement | |
enforcement.capabilities.supported required | boolean | Whether this server release enforces the component at all. All four since Phase 2. |
enforcement.capabilities.requested required | boolean | Whether the stored document has this section (capabilities = chat or attachments). |
enforcement.capabilities.applied_revision required | integer (int64), nullable | The revision the component runs at. publish: the revision every live leg has applied, null with no leg. capabilities: the stored revision — read from the membership row at every request, and by a live chat socket within about a second of the write. null when unsupported or not requested. |
enforcement.subscribe required | RtcComponentEnforcement | |
enforcement.subscribe.supported required | boolean | Whether this server release enforces the component at all. All four since Phase 2. |
enforcement.subscribe.requested required | boolean | Whether the stored document has this section (capabilities = chat or attachments). |
enforcement.subscribe.applied_revision required | integer (int64), nullable | The revision the component runs at. publish: the revision every live leg has applied, null with no leg. capabilities: the stored revision — read from the membership row at every request, and by a live chat socket within about a second of the write. null when unsupported or not requested. |
enforcement.roster required | RtcComponentEnforcement | |
enforcement.roster.supported required | boolean | Whether this server release enforces the component at all. All four since Phase 2. |
enforcement.roster.requested required | boolean | Whether the stored document has this section (capabilities = chat or attachments). |
enforcement.roster.applied_revision required | integer (int64), nullable | The revision the component runs at. publish: the revision every live leg has applied, null with no leg. capabilities: the stored revision — read from the membership row at every request, and by a live chat socket within about a second of the write. null when unsupported or not requested. |
enforcement.devices required | RtcComponentEnforcement | |
enforcement.devices.supported required | boolean | Whether this server release enforces the component at all. All four since Phase 2. |
enforcement.devices.requested required | boolean | Whether the stored document has this section (capabilities = chat or attachments). |
enforcement.devices.applied_revision required | integer (int64), nullable | The revision the component runs at. publish: the revision every live leg has applied, null with no leg. capabilities: the stored revision — read from the membership row at every request, and by a live chat socket within about a second of the write. null when unsupported or not requested. |
policy required | RtcPolicyDocument | |
policy.revision required | integer (int64) min 1 | Your monotonic revision for this member's policy. Higher than stored: replaced. Equal with the same document: no-op. Equal with a different document: 409 revision_conflict. Lower: 409 stale_revision. Restarts after a DELETE. |
policy.publish | RtcPublishPolicy | |
policy.publish.audio | boolean default true | May publish audio. |
policy.publish.video | boolean default true | May publish video (any source — the server cannot tell a screen from a camera). |
policy.publish.max_audio_tracks | integer min 0 | Most audio publications active at once. Omitted: no limit. |
policy.publish.max_video_tracks | integer min 0 | Most video publications active at once. Omitted: no limit. |
policy.subscribe | RtcSubscribePolicy | |
policy.subscribe.mode required if its parent is sent | one of: all, none, allowlist | Which publishers the member may receive. Applied by the media loop from the leg's first offer: a refused publisher's sections are never offered; a tightening stops its packets at once, removes its sections and re-offers the leg; a loosening offers the newly allowed publishers. |
policy.subscribe.publishers | array of string max 64 items | Principal ids, exact opaque strings, 1–128 characters each, at most 64; only with allowlist. A SET: duplicates are 400, the server sorts the ids canonically before storing and comparing, order never affects forwarding, and the same set in a different order at an equal revision is an idempotent no-op, not revision_conflict. GET returns the sorted list. |
policy.chat | RtcChatPolicy | |
policy.chat.send | boolean default true | May send chat messages. false: a send fails with an error ack chat_send_forbidden; the socket stays open. |
policy.chat.history | boolean default true | May read history. false also closes the chat socket with reason policy_changed. |
policy.attachments | RtcAttachmentPolicy | |
policy.attachments.upload | boolean default true | |
policy.attachments.download | boolean default true | |
policy.roster | one of: all, authorized-only, none | Who a restricted subscriber sees in the RTC roster frames (peers, peer.joined, peer.left): all, authorized-only (the publishers it may receive, nobody else) or none. Omitted: all unless subscribe.mode is none or allowlist, then authorized-only. A change re-sends the member a fresh peers snapshot under the new filter. |
policy.devices | RtcDevicePolicy | How many devices the member may have in the call at once. A device is the session token's device_id: under this policy a new leg with the same device_id as a live one is that device reconnecting and replaces its predecessor (closed 4412 device_reconnected) whatever on_limit says, never counting twice. A leg minted without a device_id is a device of its own — mint with one when using a limit, or a reconnect can be refused until the abandoned leg's keepalive gives up (up to 50 s). Counts RTC call legs only, not chat sockets. Checked at each leg's admission, and re-applied to live legs within about a second when the policy changes: a lowered limit closes the surplus at once (reject-new: the newest devices, 4411; replace-old: the oldest, 4412; allow: none). Every conflict is reported as an rtc.device_conflict webhook event. |
policy.devices.max_active required if its parent is sent | integer min 1 · max 16 | Most devices the member may have in the call at once. |
policy.devices.on_limit required if its parent is sent | one of: allow, reject-new, replace-old | allow: admit the new device anyway and only report it. reject-new: the new leg gets an error event (reason: device_limit) and is closed 4411; the devices already in the call are untouched. replace-old: the new leg is admitted and the member's oldest device is closed 4412 policy_changed. |
Errors · 401 · 403 · 404 · 429
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
429 | Rate limit exceeded |
DELETE /v1/server/rooms/{room_id}/members/{principal_id}/rtc-policy # Back to role-only rights — a write in the same revision sequence, ?revision= required (members:write)
deleteMemberRtcPolicy · scope members:write captured live
Request
curl -X DELETE https://api.ollacore.com/v1/server/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/members/alice/rtc-policy?revision=4 \
-H "Authorization: Bearer ak_live_…"
| Parameter | Type | Description |
room_id path required | string (uuid) | The room's id. A room in another tenant, or one being erased, is 404. |
principal_id path required | string | The member the policy applies to. 404 if they are not an active member of the room. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
revision query required | integer (int64) min 1 | This deletion's revision, from the same monotonic allocator as your PUTs; must be above the stored revision. Becomes the tombstone. |
If-Match header | string | Precondition: the revision you believe is stored, quoted or bare ("12"). 409 revision_conflict with current_revision when it is not. |
Response · 204
Empty body.
Errors · 400 · 401 · 403 · 404 · 409 · 429
| Status | When |
400 | revision missing or not a positive integer, or an If-Match that is not a revision. |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
409 | stale_revision: revision is below the stored one. revision_conflict: revision equals the stored one, or If-Match named a revision other than the stored one. current_revision says which. |
429 | Rate limit exceeded |
A deletion is a write in the same revision sequence: ?revision= must exceed the stored revision and stays on the row as a tombstone, so the next PUT must be higher than 4.
GET /v1/server/rooms/{room_id}/rtc/forwarding # Who receives whose media in the call right now, with the simulcast layer (rooms:read)
getRoomForwarding · scope rooms:read captured live
Request
curl -X GET https://api.ollacore.com/v1/server/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/rtc/forwarding \
-H "Authorization: Bearer ak_live_…"
| Parameter | Type | Description |
room_id path required | string (uuid) | The room's id. A room in another tenant, or one being erased, is 404. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"room_id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
"in_call": false,
"publishers": []
}
Response fields (13)
| Field | Type | Description |
room_id required | string (uuid) | |
in_call required | boolean | false when nobody is in the call; publishers is then empty. |
publishers required | array of RtcForwardingPublisher | Each connection that publishes, with what it publishes and who receives it. A connection that only receives appears as a subscriber, not here. |
publishers[].principal_id required | string | |
publishers[].client_id required | string | The publishing connection, a u64 as a decimal string. |
publishers[].tracks required | array of RtcForwardingTrack | |
publishers[].tracks[].mid required | string | The publisher's a=mid for the track. |
publishers[].tracks[].track_id | string | The publisher's own track id (what a chatbox.msid-principal subscriber reads off the msid), when the SFU has reported it. |
publishers[].tracks[].kind required | one of: audio, video | |
publishers[].tracks[].subscribers required | array of RtcForwardingSubscriber | Every connection the track is forwarded to right now. |
publishers[].tracks[].subscribers[].principal_id required | string | |
publishers[].tracks[].subscribers[].client_id required | string | The receiving connection, a u64 as a decimal string (it exceeds 2^53). |
publishers[].tracks[].subscribers[].layer required | string, nullable | The simulcast layer (RID) it receives; null for a single-encoding track or before a layer is chosen. |
Errors · 401 · 403 · 404 · 429 · 503
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
429 | Rate limit exceeded |
503 | The media thread did not answer in time (not_ready). Retry. |
Nobody is in the call during this run, so in_call is false and publishers is empty. In a live call each publishing connection lists its tracks and every connection receiving each one, with the simulcast layer.
Tokens, principals, messages #
POST /v1/server/session-tokens # Mint a room-scoped client token
createSessionToken · scope tokens:mint captured live
Request
curl -X POST https://api.ollacore.com/v1/server/session-tokens \
-H "Authorization: Bearer ak_live_…" \
-H "Content-Type: application/json" \
-d '{"room_id":"25964f12-9ae9-4356-80e8-69bc5f749bd1","principal_id":"alice","role":"owner","ttl_seconds":3600,"device_id":"dev-alice"}'
{
"room_id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
"principal_id": "alice",
"role": "owner",
"ttl_seconds": 3600,
"device_id": "dev-alice"
}
| Parameter | Type | Description |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
room_id required | string (uuid) | The room the token is for. A token opens exactly one room. |
principal_id required | string | Who the token is for. Must be an active member of the room (403 otherwise). |
role | string | Optional: mint with fewer rights than the member has. At or below the member's stored role (a higher one is 403); anything but owner|member|viewer is 400. Omit to use the member's own role. |
ttl_seconds | integer | Token lifetime in seconds. Defaults to 600 and is clamped to 60–3600. |
device_id | string | Identifies one device of the principal. Required for the E2EE endpoints, and lets the same user join a call from two devices at once. |
Response · 200
{
"access_token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImEwM2I1NjQ4MjM3YjlmZWIifQ.eyJpc3MiOiJjaGF0Ym94LWJhY2tlbmQiLCJhdWQiOiJjaGF0Ym94LXJlYWx0aW1lIiwic3ViIjoiYWxpY2UiLCJ0aWQiOiJhY2N0X2ZhYzkxYjI4MjE5MWNjMDJkZDI5OTEyYzhkYjQzZjFjIiwicmlkIjoiMjU5NjRmMTItOWFlOS00MzU2LTgwZTgtNjliYzVmNzQ5YmQxIiwicm9sZSI6Im93bmVyIiwicGVybWlzc2lvbnMiOlsiY2hhdDpzZW5kIiwiY2hhdDpoaXN0b3J5IiwiYXR0YWNobWVudDp1cGxvYWQiLCJhdHRhY2htZW50OmRvd25sb2FkIiwicnRjOnB1Ymxpc2giLCJydGM6c3Vic2NyaWJlIl0sImRpZCI6ImRldi1hbGljZSIsImp0aSI6ImU5MDg1MDAyLTFkMTAtNGE4My1hZGQyLTcxZmIyM2UxODIwMCIsImlhdCI6MTc5MjMyNDY2NywibmJmIjoxNzkyMzI0NjY3LCJleHAiOjE3OTIzMjgyNjd9.SIGNATURE_REMOVED",
"expires_at": "2026-01-15T10:30:04Z",
"rtc_websocket_url": "wss://api.ollacore.com/v1/rtc/ws",
"chat_websocket_url": "wss://api.ollacore.com/v1/chat/ws",
"ice_servers": [
{
"urls": [
"stun:api.ollacore.com:3478"
]
},
{
"urls": [
"turn:api.ollacore.com:3478",
"turns:api.ollacore.com:5349"
],
"username": "1789064267:alice",
"credential": "EXAMPLE_TURN_CREDENTIAL="
}
]
}
Response fields (8)
| Field | Type | Description |
access_token required | string | The room-scoped session token (an Ed25519-signed JWT). Present it as Authorization: Bearer … over HTTP and as the WebSocket subprotocol bearer.<token>, never in a URL. |
expires_at required | string (date-time) | When the token expires. Mint a new one before then; a socket closes with 4401 at expiry. |
rtc_websocket_url required | string (uri) | The call-signalling WebSocket URL. |
chat_websocket_url required | string (uri) | The chat WebSocket URL. |
ice_servers required | array of object | STUN and TURN servers for WebRTC, with short-lived TURN credentials. Pass them to RTCPeerConnection unchanged. |
ice_servers[].urls required | array of string | The server URLs. They never carry a ?transport= query, which Safari refuses. |
ice_servers[].username | string | expiry_unix:principal — ephemeral, 600 s |
ice_servers[].credential | string | base64(HMAC-SHA1(turn_secret, username)) |
Errors · 400 · 401 · 403 · 404 · 429
| Status | When |
400 | role is not one of owner | member | viewer. |
401 | Missing, expired or invalid credentials |
403 | The principal is not an active member of the room, the key lacks the tokens:mint scope, or role is above the member's stored role (a viewer cannot be minted an owner token). |
404 | No such room, message, member or attachment |
429 | Rate limit exceeded |
POST /v1/server/principals # Register a principal (bot/service identity)
registerPrincipal captured live
Request
curl -X POST https://api.ollacore.com/v1/server/principals \
-H "Authorization: Bearer ak_live_…" \
-H "Content-Type: application/json" \
-d '{"principal_id":"support-bot","kind":"agent"}'
{
"principal_id": "support-bot",
"kind": "agent"
}
| Parameter | Type | Description |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
principal_id required | string | The id to register, 1–128 characters. |
kind required | one of: agent, bot, service, system | agent, bot, service or system. Registering is what allows your server to send as this principal; a human cannot be registered. |
Response · 200
{
"principal_id": "support-bot",
"kind": "agent"
}
Response fields (2)
| Field | Type | Description |
principal_id required | string | The principal id you registered, 1–128 characters. |
kind required | one of: agent, bot, service, system | What kind of non-human sender this is. Only a registered principal can be sent as from POST /v1/server/rooms/{room_id}/messages. |
Errors · 401 · 403
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
POST /v1/server/principals/{principal_id}/erase # GDPR erase a principal
erasePrincipal · scope data:erase captured live
Request
curl -X POST https://api.ollacore.com/v1/server/principals/erasable-1/erase \
-H "Authorization: Bearer ak_live_…" \
-H "Content-Type: application/json" \
-d '{}'
{}
| Parameter | Type | Description |
principal_id path required | string | The principal whose data to erase, across every room of your tenant. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"erasing": true
}
Response fields (1)
| Field | Type | Description |
erasing required | boolean | True: the principal's data is being erased in the background (messages, files, memberships, receipts, reactions, push subscriptions, keys and any directory identity). |
Errors · 401 · 403
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
POST /v1/server/rooms/{room_id}/messages # Send as a principal (server-side)
sendMessage · scope messages:write captured live
Request
curl -X POST https://api.ollacore.com/v1/server/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/messages \
-H "Authorization: Bearer ak_live_…" \
-H "Content-Type: application/json" \
-d '{"sender_principal_id":"support-bot","client_message_id":"srv-8c1f2a","kind":"text","body":{"text":"Your order #7781 has shipped."}}'
{
"sender_principal_id": "support-bot",
"client_message_id": "srv-8c1f2a",
"kind": "text",
"body": {
"text": "Your order #7781 has shipped."
}
}
| Parameter | Type | Description |
room_id path required | string (uuid) | The room's id. A room in another tenant, or one being erased, is 404. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
sender_principal_id required | string | Who to send as. Must be a registered agent/bot/service/system principal and an active member of the room; sending as a human is refused (403). |
client_message_id required | string | Your idempotency key, 1–128 characters. Resending it returns the original message. |
kind | string | The message kind. Defaults to text. |
body | object | The message content: any JSON value, at most 16 KiB serialised. The server does not require a shape; clients conventionally send an object such as {"text": …}, which is what previews and search read. Required unless forward_of is set. A mentions array of principal ids (at most 50, each an active member of the room, validated on send; 400 otherwise) names members: it is stored and delivered as sent, and a mentioned member is pushed even while they have muted the room. Not on system or mls.* messages. |
reply_to | string (uuid) | Id of a message in the same room this one replies to. |
attachment_ids | array of string (uuid) | Up to 10 ready attachments from this room. |
forward_of | string (uuid) | Forward a message instead of composing one: the id of a message in any room the sender principal is an active member of (this one included). The server copies its kind, body and media into this room — kind, body and attachment_ids in this request are ignored — and the copy carries forward_depth one deeper than the source. Only text, image, voice, video, file and story messages can be forwarded, not a deleted one, and not into an E2EE room (409). A source you cannot see — unknown, another tenant's, in a room you are not in — is one 400. mentions in the source do not travel. Media is copied inside the store as ready attachments of this room, uploaded by the sender. |
Response · 200
{
"id": "e0b25ea0-1320-4862-af8d-bc28a2547b27",
"event_seq": 5200,
"room_id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
"sender_id": "support-bot",
"client_message_id": "srv-8c1f2a",
"kind": "text",
"body": {
"text": "Your order #7781 has shipped."
},
"created_at": "2026-01-15T09:30:04Z"
}
Response fields (25)
| Field | Type | Description |
id required | string (uuid) | The message's id. |
event_seq required | integer (int64) | The message's position: strictly increasing within a room, assigned at commit. Use it for ordering and for catch-up (after_seq). It is not contiguous — one sequence is shared by all rooms — so a jump between two messages does not mean one is missing. |
room_id required | string (uuid) | The room the message belongs to. |
sender_id required | string | The principal that sent the message. For a client send it comes from the session token, never the body; for a server send (POST /v1/server/rooms/{room_id}/messages) it is the sender_principal_id you named, which must be a registered non-human principal. |
client_message_id required | string | The sender's idempotency key for this message. |
kind required | one of: text, image, voice, video, story, file, system, deleted | What the message is. Once a message is soft-deleted its kind reads deleted and its body is null. |
body required | any | The message content as sent: any JSON value, at most 16 KiB serialised (conventionally {"text": …}). In an E2EE room it is {"ct": base64} ciphertext, up to 128 KiB. Null once the message is deleted. |
reply_to | string (uuid) | The id of the message this one replies to, in the same room. Absent when it is not a reply. |
forward_depth | integer min 1 | Present on a forwarded message: how many forwards deep it is (1 for a forward of an original, 2 for a forward of a forward, …). Render "Forwarded" from it, and "Forwarded many times" past a threshold of your choosing. The source message is never named — recipients may not be in the room it came from. Absent on a message composed in its room. |
created_at required | string (date-time) | When the message was committed. |
edited_at | string (date-time) | When the message was last edited. Absent if never edited. |
deleted_at | string (date-time) | When the message was soft-deleted. Absent unless deleted. |
attachments | array of AttachmentRef | The files attached to the message. Absent when there are none. |
attachments[].id required if its parent is sent | string (uuid) | The attachment's id. |
attachments[].original_name required if its parent is sent | string | The file name as uploaded (sanitised). |
attachments[].mime required if its parent is sent | string | The attachment's content type. |
attachments[].byte_size required if its parent is sent | integer (int64) | Size in bytes. |
attachments[].status required if its parent is sent | string | pending until the upload completes, uploaded while it is verified, then ready or failed. |
attachments[].width | integer | Image width in pixels, when known. |
attachments[].height | integer | Image height in pixels, when known. |
attachments[].has_thumbnail required if its parent is sent | boolean | Whether a thumbnail was generated; the download response then carries thumbnail_url. |
reactions | array of ReactionAggregate | The message's reactions, aggregated per emoji. Absent when there are none. This is the authoritative state: reaction.added/reaction.removed events are never replayed, so after a reconnect rebuild from here rather than from the events. |
reactions[].emoji required if its parent is sent | string | The emoji reacted with. |
reactions[].count required if its parent is sent | integer (int64) | How many principals have reacted with it. Always principals.length. |
reactions[].principals required if its parent is sent | array of string | Who reacted, by principal id. |
Errors · 401 · 402 · 403 · 404 · 409
| Status | When |
401 | Missing, expired or invalid credentials |
402 | The tenant has used its monthly message quota (quota_exceeded). Retrying will not help until the quota resets at the start of the month (UTC) or the plan changes. |
403 | Authenticated but not permitted here (code forbidden), or — in a direct turnkey-directory conversation — frozen by a block (code blocked): one of the two people has blocked the other, so neither sends, reacts, types or calls in it until the block is lifted. Branch on code. |
404 | No such room, message, member or attachment |
409 | Conflicts with current state |
Usage & webhooks #
Metered usage for a window
getUsage · scope usage:read captured live
Request
curl -X GET https://api.ollacore.com/v1/server/usage \
-H "Authorization: Bearer ak_live_…"
| Parameter | Type | Description |
from query | string (date-time) | Window start (inclusive). Defaults to 30 days before to. |
to query | string (date-time) | Window end (exclusive). Defaults to now. Window may not exceed 366 days. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"from": "2025-12-16T09:30:48Z",
"to": "2026-01-15T09:30:48Z",
"usage": [
{
"metric": "attachment.bytes",
"total": 160
},
{
"metric": "message.sent",
"total": 6
}
]
}
Response fields (5)
| Field | Type | Description |
from required | string (date-time) | Start of the window summed (inclusive). Defaults to 30 days before to. |
to required | string (date-time) | End of the window summed. Defaults to now; at most 366 days after from. |
usage required | array of object | One entry per metered metric with usage in the window: message.sent (count) and attachment.bytes (bytes). |
usage[].metric required | string | e.g. message.sent, attachment.bytes |
usage[].total required | integer (int64) | The metric's total over the window. |
Errors · 400 · 401 · 403
| Status | When |
400 | Invalid window (from >= to, or exceeds 366 days) |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
POST /v1/server/webhooks # Create an endpoint subscription
createWebhook · scope webhooks:manage captured live
Request
curl -X POST https://api.ollacore.com/v1/server/webhooks \
-H "Authorization: Bearer ak_live_…" \
-H "Content-Type: application/json" \
-d '{"url":"https://hooks.example.com/health","event_types":["message.created","call.started","member.added"]}'
{
"url": "https://hooks.example.com/health",
"event_types": [
"message.created",
"call.started",
"member.added"
]
}
| Parameter | Type | Description |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
url required | string (uri) | https only |
event_types required | array of one of: message.created, message.updated, message.deleted, receipt.delivered, receipt.read, reaction.added, reaction.removed, attachment.ready, attachment.failed, call.started, call.ended, member.added, member.removed, member.updated, rtc.policy.denied, rtc.device_conflict | The events to deliver, at least one. Any of the event types listed on the webhooks page. |
Response · 200
{
"id": "whe_2416dc932158009799ae0c3d084c29c0",
"url": "https://hooks.example.com/health",
"event_types": [
"message.created",
"call.started",
"member.added"
],
"secret": "whsec_EXAMPLE_NOT_A_REAL_SECRET"
}
Response fields (4)
| Field | Type | Description |
id | string | The webhook's id (whe_…). Use it to list deliveries or disable the endpoint. |
url | string | The endpoint deliveries are POSTed to. |
event_types | array of one of: message.created, message.updated, message.deleted, receipt.delivered, receipt.read, reaction.added, reaction.removed, attachment.ready, attachment.failed, call.started, call.ended, member.added, member.removed, member.updated, rtc.policy.denied, rtc.device_conflict | The events this endpoint receives. |
secret | string | The HMAC signing secret. Shown once: store it to verify the X-Ollacore-Signature header on every delivery. |
Errors · 400 · 401 · 403
| Status | When |
400 | Invalid url or event type |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
GET /v1/server/webhooks # List endpoints
listWebhooks · scope webhooks:manage captured live
Request
curl -X GET https://api.ollacore.com/v1/server/webhooks \
-H "Authorization: Bearer ak_live_…"
| Parameter | Type | Description |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
[
{
"id": "whe_2416dc932158009799ae0c3d084c29c0",
"url": "https://hooks.example.com/health",
"event_types": [
"message.created",
"call.started",
"member.added"
],
"enabled": true,
"created_at": "2026-01-15T09:30:05Z"
}
]
Response fields (5)
| Field | Type | Description |
[].id | string | The webhook's id (whe_…). |
[].url | string | The endpoint deliveries are POSTed to. |
[].event_types | array of one of: message.created, message.updated, message.deleted, receipt.delivered, receipt.read, reaction.added, reaction.removed, attachment.ready, attachment.failed, call.started, call.ended, member.added, member.removed, member.updated, rtc.policy.denied, rtc.device_conflict | The events this endpoint receives. |
[].enabled | boolean | False once the endpoint is disabled, by you or automatically after 5 deliveries in a row fail for good. |
[].created_at | string (date-time) | When the endpoint was created. |
Errors · 401 · 403
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
DELETE /v1/server/webhooks/{webhook_id} # Delete an endpoint
deleteWebhook · scope webhooks:manage captured live
Request
curl -X DELETE https://api.ollacore.com/v1/server/webhooks/whe_1fa8822227884c369fa023017fd75a2f \
-H "Authorization: Bearer ak_live_…"
| Parameter | Type | Description |
webhook_id path required | string | The webhook endpoint's id (whe_…). |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"disabled": true
}
Response fields (1)
| Field | Type | Description |
disabled | boolean | True once the endpoint stops receiving. It is disabled, not deleted: its delivery history is kept. |
Errors · 401 · 403 · 404
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
GET /v1/server/webhooks/{webhook_id}/deliveries # Delivery audit log
listWebhookDeliveries · scope webhooks:manage captured live
Request
curl -X GET https://api.ollacore.com/v1/server/webhooks/whe_2416dc932158009799ae0c3d084c29c0/deliveries \
-H "Authorization: Bearer ak_live_…"
| Parameter | Type | Description |
webhook_id path required | string | The webhook endpoint's id (whe_…). |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
[
{
"id": "whd_7d282f27fea842a9ab25a16bc1b2b72a",
"event_id": "evt_16dac5b07cf72653ebcc599c",
"event_type": "call.started",
"status": "delivered",
"attempt": 0,
"last_status_code": 200,
"last_error": null,
"created_at": "2026-01-15T09:30:33Z",
"updated_at": "2026-01-15T09:30:34Z"
},
{
"id": "whd_7eebb7ce833d4b1f8f0d05eb10f7e035",
"event_id": "evt_4656e090af7f80263ee36867",
"event_type": "message.created",
"status": "delivered",
"attempt": 0,
"last_status_code": 200,
"last_error": null,
"created_at": "2026-01-15T09:30:14Z",
"updated_at": "2026-01-15T09:30:15Z"
},
{
"id": "whd_91cf0dc2f85f4ec1a4a9718d04c5785d",
"event_id": "evt_113ded9a5ab832955cab6c67",
"event_type": "message.created",
"status": "delivered",
"attempt": 0,
"last_status_code": 200,
"last_error": null,
"created_at": "2026-01-15T09:30:08Z",
"updated_at": "2026-01-15T09:30:08Z"
},
{
"id": "whd_e7dff7fae5034b78bdc3287f48a5589d",
"event_id": "evt_1a16ba018c483822d15a987b",
"event_type": "message.created",
"status": "delivered",
"attempt": 0,
"last_status_code": 200,
"last_error": null,
"created_at": "2026-01-15T09:30:07Z",
"updated_at": "2026-01-15T09:30:08Z"
},
{
"id": "whd_2a6d536f8bd34ae6b1a0995159d87565",
"event_id": "evt_2f695376505edc586f7fffd9",
"event_type": "message.created",
"status": "delivered",
"attempt": 0,
"last_status_code": 200,
"last_error": null,
"created_at": "2026-01-15T09:30:06Z",
"updated_at": "2026-01-15T09:30:07Z"
}
]
Response fields (9)
| Field | Type | Description |
[].id | string | The delivery's id (whd_…). Replay a failed one by id. |
[].event_id | string | The event's id (evt_…), shared by every endpoint that received it. Deduplicate on it. |
[].event_type | string | The event type delivered. |
[].status | string | pending or retrying while in flight, delivered on a 2xx, failed after the last retry, cancelled if the endpoint was disabled first. |
[].attempt | integer | Retries made so far, counting from 0: a delivery that succeeded first time shows 0. A failed attempt is retried after 30 s, 2 min, 10 min, 30 min and 2 h; the sixth attempt is the last. |
[].last_status_code | integer, nullable | The HTTP status of the last attempt, or null if it never got a response. |
[].last_error | string, nullable | Why the last attempt failed, or null. |
[].created_at | string (date-time) | When the delivery was queued. |
[].updated_at | string (date-time) | When the delivery last changed state. |
Errors · 401 · 403 · 404
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |
POST /v1/server/webhooks/{webhook_id}/deliveries/{delivery_id}/replay # Replay a delivery
replayWebhookDelivery · scope webhooks:manage captured live
Request
curl -X POST https://api.ollacore.com/v1/server/webhooks/whe_2416dc932158009799ae0c3d084c29c0/deliveries/whd_7d282f27fea842a9ab25a16bc1b2b72a/replay \
-H "Authorization: Bearer ak_live_…" \
-H "Content-Type: application/json" \
-d '{}'
{}
| Parameter | Type | Description |
webhook_id path required | string | The webhook endpoint's id (whe_…). |
delivery_id path required | string | The delivery's id (whd_…), from the delivery log. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"replayed": true
}
Response fields (1)
| Field | Type | Description |
replayed | boolean | True once the delivery is queued again, from attempt 0. |
Errors · 401 · 403 · 404
| Status | When |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
404 | No such room, message, member or attachment |