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

23 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)

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 · 401 · 403 · 422 · 429
StatusWhen
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

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

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)

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

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)

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)

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)

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)

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)

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. Restarts after a DELETE.
publish RtcPublishPolicy
publish.audio boolean default true May publish audio.
publish.video boolean default true May publish video (any source — the server cannot tell a screen from a camera).
publish.max_audio_tracks integer min 0 Most audio publications active at once. Omitted: no limit.
publish.max_video_tracks integer min 0 Most video publications active at once. Omitted: no limit.
subscribe RtcSubscribePolicy
subscribe.mode required if its parent is sent one of: all, none, allowlist Which publishers the member may receive. Applied by the media loop from the leg's first offer: a refused publisher's sections are never offered; a tightening stops its packets at once, removes its sections and re-offers the leg; a loosening offers the newly allowed publishers.
subscribe.publishers array of string max 64 items Principal ids, exact opaque strings, 1–128 characters each, at most 64; only with allowlist. A SET: duplicates are 400, the server sorts the ids canonically before storing and comparing, order never affects forwarding, and the same set in a different order at an equal revision is an idempotent no-op, not revision_conflict. GET returns the sorted list.
chat RtcChatPolicy
chat.send boolean default true May send chat messages. false: a send fails with an error ack chat_send_forbidden; the socket stays open.
chat.history boolean default true May read history. false also closes the chat socket with reason policy_changed.
attachments RtcAttachmentPolicy
attachments.upload boolean default true
attachments.download boolean default true
roster one of: all, authorized-only, none Who a restricted subscriber sees in the RTC roster frames (peers, peer.joined, peer.left): all, authorized-only (the publishers it may receive, nobody else) or none. Omitted: all unless subscribe.mode is none or allowlist, then authorized-only. A change re-sends the member a fresh peers snapshot under the new filter.
devices RtcDevicePolicy How many devices the member may have in the call at once. A device is the session token's device_id: under this policy a new leg with the same device_id as a live one is that device reconnecting and replaces its predecessor (closed 4412 device_reconnected) whatever on_limit says, never counting twice. A leg minted without a device_id is a device of its own — mint with one when using a limit, or a reconnect can be refused until the abandoned leg's keepalive gives up (up to 50 s). Counts RTC call legs only, not chat sockets. Checked at each leg's admission, and re-applied to live legs within about a second when the policy changes: a lowered limit closes the surplus at once (reject-new: the newest devices, 4411; replace-old: the oldest, 4412; allow: none). Every conflict is reported as an rtc.device_conflict webhook event.
devices.max_active required if its parent is sent integer min 1 · max 16 Most devices the member may have in the call at once.
devices.on_limit required if its parent is sent one of: allow, reject-new, replace-old allow: admit the new device anyway and only report it. reject-new: the new leg gets an error event (reason: device_limit) and is closed 4411; the devices already in the call are untouched. replace-old: the new leg is admitted and the member's oldest device is closed 4412 policy_changed.

Response · 200

{
  "principal_id": "alice",
  "stored_revision": 3,
  "applied_revision": null,
  "applied": false,
  "applied_at": null,
  "in_call": false,
  "active_legs": 0,
  "applied_legs": 0,
  "subscribe_enforced": true,
  "enforcement": {
    "publish": {
      "supported": true,
      "requested": true,
      "applied_revision": null
    },
    "capabilities": {
      "supported": true,
      "requested": true,
      "applied_revision": 3
    },
    "subscribe": {
      "supported": true,
      "requested": true,
      "applied_revision": null
    },
    "roster": {
      "supported": true,
      "requested": false,
      "applied_revision": null
    },
    "devices": {
      "supported": true,
      "requested": false,
      "applied_revision": null
    }
  },
  "policy": {
    "revision": 3,
    "publish": {
      "audio": false,
      "video": true,
      "max_audio_tracks": 0,
      "max_video_tracks": 1
    },
    "subscribe": {
      "mode": "allowlist",
      "publishers": [
        "host"
      ]
    },
    "chat": {
      "send": false,
      "history": false
    },
    "attachments": {
      "upload": false,
      "download": false
    }
  }
}
Response fields (50)
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. Restarts after a DELETE.
policy.publish RtcPublishPolicy
policy.publish.audio boolean default true May publish audio.
policy.publish.video boolean default true May publish video (any source — the server cannot tell a screen from a camera).
policy.publish.max_audio_tracks integer min 0 Most audio publications active at once. Omitted: no limit.
policy.publish.max_video_tracks integer min 0 Most video publications active at once. Omitted: no limit.
policy.subscribe RtcSubscribePolicy
policy.subscribe.mode required if its parent is sent one of: all, none, allowlist Which publishers the member may receive. Applied by the media loop from the leg's first offer: a refused publisher's sections are never offered; a tightening stops its packets at once, removes its sections and re-offers the leg; a loosening offers the newly allowed publishers.
policy.subscribe.publishers array of string max 64 items Principal ids, exact opaque strings, 1–128 characters each, at most 64; only with allowlist. A SET: duplicates are 400, the server sorts the ids canonically before storing and comparing, order never affects forwarding, and the same set in a different order at an equal revision is an idempotent no-op, not revision_conflict. GET returns the sorted list.
policy.chat RtcChatPolicy
policy.chat.send boolean default true May send chat messages. false: a send fails with an error ack chat_send_forbidden; the socket stays open.
policy.chat.history boolean default true May read history. false also closes the chat socket with reason policy_changed.
policy.attachments RtcAttachmentPolicy
policy.attachments.upload boolean default true
policy.attachments.download boolean default true
policy.roster one of: all, authorized-only, none Who a restricted subscriber sees in the RTC roster frames (peers, peer.joined, peer.left): all, authorized-only (the publishers it may receive, nobody else) or none. Omitted: all unless subscribe.mode is none or allowlist, then authorized-only. A change re-sends the member a fresh peers snapshot under the new filter.
policy.devices RtcDevicePolicy How many devices the member may have in the call at once. A device is the session token's device_id: under this policy a new leg with the same device_id as a live one is that device reconnecting and replaces its predecessor (closed 4412 device_reconnected) whatever on_limit says, never counting twice. A leg minted without a device_id is a device of its own — mint with one when using a limit, or a reconnect can be refused until the abandoned leg's keepalive gives up (up to 50 s). Counts RTC call legs only, not chat sockets. Checked at each leg's admission, and re-applied to live legs within about a second when the policy changes: a lowered limit closes the surplus at once (reject-new: the newest devices, 4411; replace-old: the oldest, 4412; allow: none). Every conflict is reported as an rtc.device_conflict webhook event.
policy.devices.max_active required if its parent is sent integer min 1 · max 16 Most devices the member may have in the call at once.
policy.devices.on_limit required if its parent is sent one of: allow, reject-new, replace-old allow: admit the new device anyway and only report it. reject-new: the new leg gets an error event (reason: device_limit) and is closed 4411; the devices already in the call are untouched. replace-old: the new leg is admitted and the member's oldest device is closed 4412 policy_changed.
Errors · 400 · 401 · 403 · 404 · 409 · 429
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)

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. Restarts after a DELETE.
policy.publish RtcPublishPolicy
policy.publish.audio boolean default true May publish audio.
policy.publish.video boolean default true May publish video (any source — the server cannot tell a screen from a camera).
policy.publish.max_audio_tracks integer min 0 Most audio publications active at once. Omitted: no limit.
policy.publish.max_video_tracks integer min 0 Most video publications active at once. Omitted: no limit.
policy.subscribe RtcSubscribePolicy
policy.subscribe.mode required if its parent is sent one of: all, none, allowlist Which publishers the member may receive. Applied by the media loop from the leg's first offer: a refused publisher's sections are never offered; a tightening stops its packets at once, removes its sections and re-offers the leg; a loosening offers the newly allowed publishers.
policy.subscribe.publishers array of string max 64 items Principal ids, exact opaque strings, 1–128 characters each, at most 64; only with allowlist. A SET: duplicates are 400, the server sorts the ids canonically before storing and comparing, order never affects forwarding, and the same set in a different order at an equal revision is an idempotent no-op, not revision_conflict. GET returns the sorted list.
policy.chat RtcChatPolicy
policy.chat.send boolean default true May send chat messages. false: a send fails with an error ack chat_send_forbidden; the socket stays open.
policy.chat.history boolean default true May read history. false also closes the chat socket with reason policy_changed.
policy.attachments RtcAttachmentPolicy
policy.attachments.upload boolean default true
policy.attachments.download boolean default true
policy.roster one of: all, authorized-only, none Who a restricted subscriber sees in the RTC roster frames (peers, peer.joined, peer.left): all, authorized-only (the publishers it may receive, nobody else) or none. Omitted: all unless subscribe.mode is none or allowlist, then authorized-only. A change re-sends the member a fresh peers snapshot under the new filter.
policy.devices RtcDevicePolicy How many devices the member may have in the call at once. A device is the session token's device_id: under this policy a new leg with the same device_id as a live one is that device reconnecting and replaces its predecessor (closed 4412 device_reconnected) whatever on_limit says, never counting twice. A leg minted without a device_id is a device of its own — mint with one when using a limit, or a reconnect can be refused until the abandoned leg's keepalive gives up (up to 50 s). Counts RTC call legs only, not chat sockets. Checked at each leg's admission, and re-applied to live legs within about a second when the policy changes: a lowered limit closes the surplus at once (reject-new: the newest devices, 4411; replace-old: the oldest, 4412; allow: none). Every conflict is reported as an rtc.device_conflict webhook event.
policy.devices.max_active required if its parent is sent integer min 1 · max 16 Most devices the member may have in the call at once.
policy.devices.on_limit required if its parent is sent one of: allow, reject-new, replace-old allow: admit the new device anyway and only report it. reject-new: the new leg gets an error event (reason: device_limit) and is closed 4411; the devices already in the call are untouched. replace-old: the new leg is admitted and the member's oldest device is closed 4412 policy_changed.
Errors · 401 · 403 · 404 · 429
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)

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
404No such room, message, member or attachment
409stale_revision: revision is below the stored one. revision_conflict: revision equals the stored one, or If-Match named a revision other than the stored one. current_revision says which.
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)

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.

Tokens, principals, messages #

POST /v1/server/session-tokens #

Mint a room-scoped client token

createSessionToken · scope tokens:mint captured live

Request

curl -X POST https://api.ollacore.com/v1/server/session-tokens \
  -H "Authorization: Bearer ak_live_…" \
  -H "Content-Type: application/json" \
  -d '{"room_id":"25964f12-9ae9-4356-80e8-69bc5f749bd1","principal_id":"alice","role":"owner","ttl_seconds":3600,"device_id":"dev-alice"}'
{
  "room_id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
  "principal_id": "alice",
  "role": "owner",
  "ttl_seconds": 3600,
  "device_id": "dev-alice"
}
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 · 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
429Rate limit exceeded
POST /v1/server/principals #

Register a principal (bot/service identity)

registerPrincipal captured live

Request

curl -X POST https://api.ollacore.com/v1/server/principals \
  -H "Authorization: Bearer ak_live_…" \
  -H "Content-Type: application/json" \
  -d '{"principal_id":"support-bot","kind":"agent"}'
{
  "principal_id": "support-bot",
  "kind": "agent"
}
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 · 401 · 403
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
POST /v1/server/principals/{principal_id}/erase #

GDPR erase a principal

erasePrincipal · scope data:erase captured live

Request

curl -X POST https://api.ollacore.com/v1/server/principals/erasable-1/erase \
  -H "Authorization: Bearer ak_live_…" \
  -H "Content-Type: application/json" \
  -d '{}'
{}
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)

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",
  "client_message_id": "srv-8c1f2a",
  "kind": "text",
  "body": {
    "text": "Your order #7781 has shipped."
  },
  "created_at": "2026-01-15T09:30:04Z"
}
Response fields (25)
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.
client_message_id required string The sender's idempotency key for this message.
kind required one of: text, image, voice, video, story, file, system, deleted What the message is. Once a message is soft-deleted its kind reads deleted and its body is null.
body required any The message content as sent: any JSON value, at most 16 KiB serialised (conventionally {"text": …}). In an E2EE room it is {"ct": base64} ciphertext, up to 128 KiB. Null once the message is deleted.
reply_to string (uuid) The id of the message this one replies to, in the same room. Absent when it is not a reply.
forward_depth integer min 1 Present on a forwarded message: how many forwards deep it is (1 for a forward of an original, 2 for a forward of a forward, …). Render "Forwarded" from it, and "Forwarded many times" past a threshold of your choosing. The source message is never named — recipients may not be in the room it came from. Absent on a message composed in its room.
created_at required string (date-time) When the message was committed.
edited_at string (date-time) When the message was last edited. Absent if never edited.
deleted_at string (date-time) When the message was soft-deleted. Absent unless deleted.
attachments array of AttachmentRef The files attached to the message. Absent when there are none.
attachments[].id required if its parent is sent string (uuid) The attachment's id.
attachments[].original_name required if its parent is sent string The file name as uploaded (sanitised).
attachments[].mime required if its parent is sent string The attachment's content type.
attachments[].byte_size required if its parent is sent integer (int64) Size in bytes.
attachments[].status required if its parent is sent string pending until the upload completes, uploaded while it is verified, then ready or failed.
attachments[].width integer Image width in pixels, when known.
attachments[].height integer Image height in pixels, when known.
attachments[].has_thumbnail required if its parent is sent boolean Whether a thumbnail was generated; the download response then carries thumbnail_url.
reactions array of ReactionAggregate The message's reactions, aggregated per emoji. Absent when there are none. This is the authoritative state: reaction.added/reaction.removed events are never replayed, so after a reconnect rebuild from here rather than from the events.
reactions[].emoji required if its parent is sent string The emoji reacted with.
reactions[].count required if its parent is sent integer (int64) How many principals have reacted with it. Always principals.length.
reactions[].principals required if its parent is sent array of string Who reacted, by principal id.
Errors · 401 · 402 · 403 · 404 · 409
StatusWhen
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

Usage & webhooks #

GET /v1/server/usage #

Metered usage for a window

getUsage · scope usage:read captured live

Request

curl -X GET https://api.ollacore.com/v1/server/usage \
  -H "Authorization: Bearer ak_live_…"
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

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.
Errors · 400 · 401 · 403
StatusWhen
400Invalid url or event type
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
GET /v1/server/webhooks #

List endpoints

listWebhooks · scope webhooks:manage captured live

Request

curl -X GET https://api.ollacore.com/v1/server/webhooks \
  -H "Authorization: Bearer ak_live_…"
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

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

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

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.