API reference

Customer server API

Your backend calls these. Tenant comes from the key — a tenant_id in a body is rejected. Scope-gated.

Credential: ak_ server key · scheme ak_ server key

24 operations · 23 captured against the live API.

Every example below is a real exchange recorded against a throwaway tenant, then redacted — not a shape transcribed from a schema. Ids and timestamps are stable fixtures so the same request always reads the same way. Back to the full index →

Rooms & members #

POST /v1/server/rooms #

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"
}
ParameterTypeDescription
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

FieldTypeDescription
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)
FieldTypeDescription
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
StatusWhen
400A created_by of the wrong length, or an invalid e2ee_scheme.
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
422Unknown field (e.g. a supplied tenant_id) rejected
429Rate 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_…"
ParameterTypeDescription
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)
FieldTypeDescription
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
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No 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_…"
ParameterTypeDescription
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)
FieldTypeDescription
deleting required boolean True: the room is inaccessible now, and its rows and files are erased in the background.
Errors · 401 · 403 · 404
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No 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"
}
ParameterTypeDescription
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

FieldTypeDescription
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)
FieldTypeDescription
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
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No such room, message, member or attachment
409Conflicts 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_…"
ParameterTypeDescription
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)
FieldTypeDescription
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
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No 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"
}
ParameterTypeDescription
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

FieldTypeDescription
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)
FieldTypeDescription
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
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No such room, message, member or attachment
422A 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_…"
ParameterTypeDescription
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)
FieldTypeDescription
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
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No 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"
    }
  ]
}
ParameterTypeDescription
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

FieldTypeDescription
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)
FieldTypeDescription
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
StatusWhen
400More than 128 entries, a publisher outside 1..=128 characters, a track_id outside 1..=256 characters, or the same publisher and track listed twice.
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No such room, message, member or attachment
422A 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_…"
ParameterTypeDescription
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)
FieldTypeDescription
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
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No 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
  }
}
ParameterTypeDescription
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

FieldTypeDescription
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)
FieldTypeDescription
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
StatusWhen
400revision 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.
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No such room, message, member or attachment
409stale_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.
429Rate 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_…"
ParameterTypeDescription
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)
FieldTypeDescription
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
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No such room, message, member or attachment
429Rate 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_…"
ParameterTypeDescription
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
StatusWhen
400revision missing or not a positive integer, or an If-Match that is not a revision.
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404not_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.
409stale_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.
429Rate 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_…"
ParameterTypeDescription
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)
FieldTypeDescription
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
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No such room, message, member or attachment
429Rate limit exceeded
503The 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_…"
ParameterTypeDescription
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
StatusWhen
400client_id is not a decimal connection id.
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No such room, message, member or attachment
429Rate limit exceeded
503The 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"
}
ParameterTypeDescription
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

FieldTypeDescription
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)
FieldTypeDescription
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
StatusWhen
400role is not one of owner | member | viewer.
401Missing, expired or invalid credentials
403The 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).
404No such room, message, member or attachment
409The room is closed: no new tokens are minted for it.
429Rate 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"
}
ParameterTypeDescription
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

FieldTypeDescription
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)
FieldTypeDescription
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
StatusWhen
400kind is not agent, bot, service or system, or principal_id is outside 1–128 characters.
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
422A 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 '{}'
{}
ParameterTypeDescription
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)
FieldTypeDescription
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
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, 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."
  }
}
ParameterTypeDescription
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

FieldTypeDescription
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)
FieldTypeDescription
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
StatusWhen
400A bad kind, client_message_id, mentions, reply_to, attachment or forward_of.
401Missing, expired or invalid credentials
402The 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.
403Authenticated 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.
404No such room, message, member or attachment
409Conflicts with current state
413The body is over 16 KiB, or over 128 KiB for an mls.* kind.

Usage & webhooks #

GET /v1/server/usage #

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_…"
ParameterTypeDescription
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)
FieldTypeDescription
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
StatusWhen
400Invalid window (from >= to, or exceeds 366 days)
401Missing, expired or invalid credentials
403Authenticated, 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"
  ]
}
ParameterTypeDescription
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

FieldTypeDescription
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)
FieldTypeDescription
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
StatusWhen
400Invalid url or event type
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
409The tenant already has 10 enabled endpoints. Delete one first.
422A 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_…"
ParameterTypeDescription
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)
FieldTypeDescription
[].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
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, 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_…"
ParameterTypeDescription
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)
FieldTypeDescription
disabled boolean True once the endpoint stops receiving. It is disabled, not deleted: its delivery history is kept.
Errors · 401 · 403 · 404
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No 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_…"
ParameterTypeDescription
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)
FieldTypeDescription
[].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
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No 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 '{}'
{}
ParameterTypeDescription
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)
FieldTypeDescription
replayed boolean True once the delivery is queued again, from attempt 0.
Errors · 401 · 403 · 404
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
404No such room, message, member or attachment

Errors on this plane #

Every failure uses the same body — code and message. These were triggered deliberately against the live API.

401 · no Authorization header

{
  "code": "unauthorized",
  "message": "unauthorized: missing Authorization header"
}

422 · unknown field: the tenant comes from the key, never the body

Failed to deserialize the JSON body into the target type: tenant_id: unknown field `tenant_id`, expected one of `created_by`, `external_ref`, `expires_at`, `e2ee`, `e2ee_scheme` at line 1 column 33

404 · unknown id — identical to a room in another tenant, deliberately

{
  "code": "not_found",
  "message": "not found"
}

The full code list and retry guidance is on Errors & rate limits.