Your backend calls these. Tenant comes from the key — a tenant_id in a body is rejected. Scope-gated.
24 operations · 23 captured against the live API.
Rooms & members #
Provision a room (idempotent on external_ref)
Requires rooms:write. The room is created in the key's tenant (a test key's sandbox) and has no members; add them with POST /v1/server/rooms/{room_id}/members. With external_ref the call is idempotent per tenant: repeating it returns the existing room unchanged and ignores the other fields. Without external_ref, every call creates a new room. e2ee and e2ee_scheme (mls or megolm, which requires e2ee: true) are set at creation and cannot change. expires_at is informational only: nothing closes or deletes the room when it passes.
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 · 400 · 401 · 403 · 422 · 429
| Status | When |
400 | A created_by of the wrong length, or an invalid e2ee_scheme. |
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
Requires rooms:read. A room in another tenant, a room that does not exist and a room being erased all return 404. status is active or closed. expires_at is informational only.
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
Marks the room deleting and asynchronously purges its object-store blobs, then the stored copies of its events (webhook deliveries, pending fan-out and push work), then cascades the row delete. Requires scope rooms:delete.
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)
A room holds at most 5,000 active members, on every plane; once it is full the add is refused with 409 and nothing changes. Restoring a removed member or changing an existing member's role never needs a seat.
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
Requires members:write. Sets the membership's left_at; the room's history is kept, and adding the principal again restores the membership. The removal takes effect at once: the member's live chat and RTC sockets close with 4403, and their HTTP requests and new session tokens are refused. A durable member.removed event goes to the room's connected clients and to webhooks subscribed to it. E2EE clients use this event as the cue to rotate keys. 404 if the principal is not an active 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)
Silence push notifications from this room for one member — eight hours, one week, or until unmuted — on behalf of a user whose settings your own server holds (a turnkey-directory user does this for themselves with PUT /v1/directory/conversations/{room_id}/mute). Push only: live delivery and call rings are unaffected, and no event is emitted. Requires 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)
Push notifications again for this member. Idempotent. Requires members:write.
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)
Which simulcast layer of which publisher's track this member should receive, chosen by your server instead of by the adaptive policy — a "pin this speaker in high quality" control. The whole list is replaced: an entry with layer: auto clears that track, an empty list clears everything. Each entry is keyed the way the member's client already sees the track: the publisher's principal id and the publisher's own track id, exactly the stream id and track id a chatbox.msid-principal socket reads off the msid. low, medium and high name the publisher's layers by measured bitrate (with two layers medium is the dearer); a pinned layer that stops is replaced by the dearest live layer below it and taken back when it returns. Applied on the media loop's next layer report, within about a second, to every connection the member has; the size cap the adaptive policy applies in large calls does not apply to a pin. Held for the member's session: a member who is not in the call yet, or whose last connection just closed, keeps the list for a 30 s grace, so a PeerConnection rebuild finds it and a member who left for good does not. Requires 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)
What is pinned for this member right now and whether they are in the call. An empty list for a member with no preference, which is every member by default. Requires 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)
Replace the member's media policy — what they may publish and receive, who they see in the roster, plus the chat and attachment rights the same token would otherwise carry. A policy is a RESTRICTION set: effective rights are token ∩ the member's current role ∩ this document, never more. Whole-document replacement under the revision rules: a higher revision is stored; the same revision with an identical document (subscribe.publishers compared as a set) is a no-op; the same revision with a different document is 409 revision_conflict; a lower revision is 409 stale_revision — both carry current_revision. The sequence is monotonic for the member's lifetime: a DELETE leaves its revision as a tombstone the next PUT must exceed. Enforced where every request already checks membership: the next HTTP request, and the chat and RTC sockets within about a second (the member's live sockets are told within this request and re-read the row; the chat socket's 30-second recheck remains the fallback). Each enforcement reads role and policy from one membership-row snapshot: a role raised later cannot grant beyond the token, a role lowered removes rights on this same schedule. On the RTC socket a publication the policy refuses is answered inactive and reported as a structured error event; the offer completes normally. Stored on the membership row, so it survives removal and restoration; DELETE is the only way back to role-only behaviour. Requires 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. Never restarts: after a DELETE, the next revision must exceed the deletion's tombstone. |
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. Never restarts: after a DELETE, the next revision must exceed the deletion's tombstone. |
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)
The stored document with stored_revision, and the engine's view of the member's live legs: applied_revision is the revision every active leg runs (null with no leg — nothing live to apply, the stored revision applies at the next registration), applied is whether it equals the stored one, active_legs and applied_legs show a change still landing. 404 for a member with no policy: a policy that exists restricts, one that does not does not, and a reconciler must be able to tell the two apart. Requires 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. Never restarts: after a DELETE, the next revision must exceed the deletion's tombstone. |
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)
Back to token ∩ current role from the next evaluation, on the same schedule as a policy change. A deletion is a write in the same revision sequence as PUT: revision (required) must be above the stored one and becomes the row's tombstone — 409 stale_revision below the stored revision, 409 revision_conflict at it, both carrying current_revision. Optionally If-Match: "<revision>" states the revision you believe is stored; if it is not, 409 revision_conflict with the actual one and nothing changes. After deletion GET answers 404; a retry of the same deletion (same revision) answers 204 again with no further effect, a DELETE below the tombstone is 409 stale_revision, and one above it is 404 policy_deleted carrying the tombstone as current_revision. The If-Match check and the write are one atomic step, so a concurrent PUT cannot slip between them. The next PUT must exceed the tombstone (at it: revision_conflict; below it: stale_revision; both report the tombstone as current_revision) — so a delayed write allocated before the deletion can never bring an obsolete policy back. The sequence never restarts for the member's lifetime. 404 when the member has no policy. Requires 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 | not_found: not an active member, or no policy was ever written for them. policy_deleted: the policy was already deleted at a different revision than this request's (current_revision is that deletion's); this request's deletion did not happen. |
409 | stale_revision: revision is below the stored policy's or the tombstone's. revision_conflict: revision equals the stored policy's, 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)
The SFU's forwarding table for the room right now: each publishing connection, its tracks, and every connection each track is forwarded to (with the simulcast layer). Lets your server reconcile what its RTC policies intend against what is actually flowing. Read from the room's media thread at request time; in_call: false with no publishers when nobody is in the call. 503 not_ready if the media thread does not answer within two seconds — retry. Requires 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.
DELETE /v1/server/rooms/{room_id}/rtc/connections/{client_id} # Close one of a member's call connections (4413), keeping the membership — their other devices stay in (members:write)
Closes exactly one of a member's RTC connections. The connection is closed with WebSocket code 4413 reason connection_closed, and the answer is 204 only once its media has been retired from the SFU (nothing it published is forwarded, nothing is forwarded to it) — not merely once a close was requested. 503 not_ready if that has not happened within three seconds: retry, and a 404 then means it finished. 404 when the id is not a live connection of this room. The member stays a member and the same token can join again; to stop a member altogether, remove them. Requires members:write.
closeRtcConnection · scope members:write from schema placeholder credentials
Request
curl -X DELETE https://api.ollacore.com/v1/server/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/rtc/connections/7 \
-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. |
client_id path required | string | The connection's id as a decimal string, as a roster (peers) or a forwarding read reports it. One connection, not a person: a member's other devices are untouched. |
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 · 204
Empty body.
Errors · 400 · 401 · 403 · 404 · 429 · 503
| Status | When |
400 | client_id is not a decimal connection id. |
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 connection is closing but its media is not retired yet (not_ready). Retry. |
Synthetic wire-shape illustration, not a captured result: a live call is needed to have a connection to close, and none is open during a capture run. client_id 7 is a non-secret fixture, not an issued connection. 204 is answered only once the connection's media is retired from the SFU; the closed socket receives WebSocket close 4413 "connection_closed". The member stays a member and the same token can join again.
Tokens, principals, messages #
POST /v1/server/session-tokens # Mint a room-scoped client token
Requires tokens:mint. Mints a signed token for one principal in one room of the key's tenant; the caller cannot choose the tenant. role defaults to the member's stored role and cannot be above it, and ttl_seconds defaults to 600, clamped to 60–3600. device_id is carried in the token so each of a principal's devices gets its own call leg. The bundle also holds the chat and RTC WebSocket URLs and ICE servers with TURN credentials. A room whose status is closed is refused with 409. Revoking the key does not invalidate tokens it already minted; removing the member does.
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 · 409 · 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 |
409 | The room is closed: no new tokens are minted for it. |
429 | Rate limit exceeded |
POST /v1/server/principals # Register a principal (bot/service identity)
Requires members:write. Records principal_id (1–128 characters) in the key's tenant as a non-human sender of kind agent, bot, service or system. Registering it again replaces the kind. Registration is what allows POST /v1/server/rooms/{room_id}/messages to send as this principal, and those messages carry its sender_kind; messages the principal sends with a session token do not. Registering does not add the principal to any room.
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 · 400 · 401 · 403 · 422
| Status | When |
400 | kind is not agent, bot, service or system, or principal_id is outside 1–128 characters. |
401 | Missing, expired or invalid credentials |
403 | Authenticated, but the token is not scoped to this room or principal |
422 | A field the operation does not take, such as tenant_id (the tenant comes from the key). |
POST /v1/server/principals/{principal_id}/erase # GDPR erase a principal
Async: hard-deletes the principal's messages, memberships, uploaded attachment blobs, receipts, reactions, push subscriptions and E2EE key material; for a directory user, also their profile, OTP challenges and sessions. Every stored copy of their events goes too: the webhook delivery log and any fan-out or push work not yet done. Requires scope data:erase.
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)
Requires messages:write. The sender must be registered with POST /v1/server/principals and be an active member of the room; any other principal, including any human, is refused with 403. The message carries sender_kind and goes through the same path as a client send: it is persisted, then published as message.created to the room's sockets, webhooks and push. It is idempotent per room and sender on client_message_id (1–128 characters): a repeat returns the original message and does not count against the quota again. kind is one of text, image, voice, video, story, file or system, with a body of at most 16 KiB. An e2ee room accepts only mls.* kinds, up to 128 KiB, and anything else is refused with 409. A message has at most 10 attachments, each already ready in this room, and at most 50 mentions, all active members. With forward_of, the server copies the source message's kind, body and media; the sender must be a member of the source room, and the destination must not be e2ee.
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",
"sender_kind": "agent",
"client_message_id": "srv-8c1f2a",
"kind": "text",
"body": {
"text": "Your order #7781 has shipped."
},
"created_at": "2026-01-15T09:30:04Z"
}
Response fields (26)
| 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. |
sender_kind | one of: agent, bot, service, system | Immutable non-human sender classification captured when a server send creates the message. It survives replay, edit, delete, history, search, realtime events and webhooks. Absent for human-authored messages. |
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 · 400 · 401 · 402 · 403 · 404 · 409 · 413
| Status | When |
400 | A bad kind, client_message_id, mentions, reply_to, attachment or forward_of. |
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 |
413 | The body is over 16 KiB, or over 128 KiB for an mls.* kind. |
Usage & webhooks #
Metered usage for a window
Requires usage:read. Sums the tenant's metered events over [from, to), which defaults to the 30 days ending now. The metrics are message.sent, one per new message (an idempotent repeat is not counted), and attachment.bytes, counted once an attachment is verified ready (failed uploads are not counted). A metric with no events in the window is left out. A test key sees only its sandbox's usage.
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
Requires webhooks:manage. url must be https, and its host must resolve within 3 seconds to public addresses only. Private, loopback, link-local and similar ranges are refused with 400, and the same check runs again on every delivery. event_types must name at least one supported type. A tenant can have at most 10 enabled endpoints, and creating one beyond that is refused with 409. The response returns the HMAC signing secret only this once; store it to verify X-Ollacore-Signature. An endpoint cannot be edited: to change one, create a new endpoint and delete the old one.
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. Each delivery is a JSON envelope {version, id, type, tenant_id, room_id, occurred_at, data} (version is 1) of at most 262144 bytes; see API.md, "Webhooks". |
Errors · 400 · 401 · 403 · 409 · 422
| 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 |
409 | The tenant already has 10 enabled endpoints. Delete one first. |
422 | A field the operation does not take, such as tenant_id (the tenant comes from the key). |
GET /v1/server/webhooks # List endpoints
Requires webhooks:manage. Returns every endpoint of the key's tenant, newest first. Disabled endpoints are included with enabled: false, whether they were disabled by DELETE or automatically after five consecutive failed deliveries. The signing secret is never returned.
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
Requires webhooks:manage. Sets enabled: false on the endpoint. The endpoint and its delivery history stay listed, and it stops counting toward the 10-endpoint limit. Deliveries that are still pending or waiting for a retry are cancelled instead of sent, and a replay to the endpoint is refused with 409. A disabled endpoint cannot be enabled again; 404 if the id is unknown or the endpoint is already disabled.
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
Newest first. A delivery is kept until 30 days after it settles (delivered, failed or cancelled), then deleted; one still pending or retrying is kept until it settles. Erasing a room, a principal or the tenant deletes the deliveries of the events erased, and scheduled retention deletes those of the messages it expires.
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
Re-sends a stored delivery to its endpoint, with the same event id. Possible until 30 days after the delivery settled; after that, or once an erasure or retention removed it, the delivery no longer exists and this returns 404.
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 |