API reference

Client API

What your app calls directly. Every path is authorised against the token’s room and principal.

Credential: room-scoped session token · scheme room session token

30 operations · 30 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 →

Messages #

POST /v1/rooms/{room_id}/messages #

Send

postMessage captured live

Request

curl -X POST https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/messages \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"client_message_id":"c-9f3b7e21","kind":"text","body":{"text":"Hey — are we still on for 3pm?"}}'
{
  "client_message_id": "c-9f3b7e21",
  "kind": "text",
  "body": {
    "text": "Hey — are we still on for 3pm?"
  }
}
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
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
client_message_id required string max length 128 Your idempotency key, 1–128 characters. Resending the same key returns the original message instead of creating a duplicate, so generate it once per message and reuse it on retries.
kind one of: text, image, voice, video, story, file, system, mls.commit, mls.proposal, mls.application, mls.welcome default "text" The message kind. Defaults to text. Plaintext rooms accept the plain kinds; an E2EE room accepts only the mls.* kinds, and a plain room refuses them (409).
body any 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. For mls.* kinds it must be {"ct": base64}, up to 128 KiB. 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) max 10 items Up to 10 attachments to attach, each already ready and uploaded to this room.
forward_of string (uuid) Forward a message instead of composing one: the id of a message in any room you are 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 you.

Response · 200

{
  "duplicate": false,
  "message": {
    "body": {
      "text": "Hey — are we still on for 3pm?"
    },
    "client_message_id": "c-9f3b7e21",
    "created_at": "2026-01-15T09:30:06Z",
    "event_seq": 5202,
    "id": "e8b186e4-845a-41dc-bb28-102dd8d0cb8d",
    "kind": "text",
    "room_id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
    "sender_id": "alice"
  }
}
Response fields (27)
FieldTypeDescription
message required Message The message as stored.
message.id required string (uuid) The message's id.
message.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.
message.room_id required string (uuid) The room the message belongs to.
message.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.
message.client_message_id required string The sender's idempotency key for this message.
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.
message.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.
message.reply_to string (uuid) The id of the message this one replies to, in the same room. Absent when it is not a reply.
message.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.
message.created_at required string (date-time) When the message was committed.
message.edited_at string (date-time) When the message was last edited. Absent if never edited.
message.deleted_at string (date-time) When the message was soft-deleted. Absent unless deleted.
message.attachments array of AttachmentRef The files attached to the message. Absent when there are none.
message.attachments[].id required if its parent is sent string (uuid) The attachment's id.
message.attachments[].original_name required if its parent is sent string The file name as uploaded (sanitised).
message.attachments[].mime required if its parent is sent string The attachment's content type.
message.attachments[].byte_size required if its parent is sent integer (int64) Size in bytes.
message.attachments[].status required if its parent is sent string pending until the upload completes, uploaded while it is verified, then ready or failed.
message.attachments[].width integer Image width in pixels, when known.
message.attachments[].height integer Image height in pixels, when known.
message.attachments[].has_thumbnail required if its parent is sent boolean Whether a thumbnail was generated; the download response then carries thumbnail_url.
message.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.
message.reactions[].emoji required if its parent is sent string The emoji reacted with.
message.reactions[].count required if its parent is sent integer (int64) How many principals have reacted with it. Always principals.length.
message.reactions[].principals required if its parent is sent array of string Who reacted, by principal id.
duplicate required boolean true when client_message_id was already persisted; the original message is returned
Errors · 401 · 402 · 403 · 413 · 429
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.
413Body exceeds MAX_UPLOAD_BYTES or the per-message limit
429Rate limit exceeded
GET /v1/rooms/{room_id}/messages #

History (after_seq / before_seq / limit)

listMessages captured live

Request

curl -X GET https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/messages?limit=20 \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…"
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
after_seq query integer (int64) Only messages with event_seq greater than this. After a reconnect, pass the last event_seq you processed. With neither cursor, the page starts at the room's oldest message; with before_seq alone, it pages backwards (see before_seq).
before_seq query integer (int64) Only messages with event_seq less than this. Given without after_seq, it pages backwards: the page is the limit messages just before before_seq, still oldest first. To keep scrolling up, pass the first message's event_seq as the next before_seq. With after_seq as well, the page starts after after_seq and stops before before_seq.
limit query integer default 100 · max 500 Maximum messages, 1–500. Defaults to 100. Counted from the oldest matching message, or from the newest when paging backwards with before_seq alone.
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

{
  "messages": [
    {
      "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"
    },
    {
      "id": "e8b186e4-845a-41dc-bb28-102dd8d0cb8d",
      "event_seq": 5202,
      "room_id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
      "sender_id": "alice",
      "client_message_id": "c-9f3b7e21",
      "kind": "text",
      "body": {
        "text": "Hey — are we still on for 3pm?"
      },
      "created_at": "2026-01-15T09:30:06Z"
    }
  ],
  "latest_seq": 5202
}
Response fields (27)
FieldTypeDescription
messages required array of Message The messages. From history, oldest first (ascending event_seq); from search, best match first, then newest.
messages[].id required string (uuid) The message's id.
messages[].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.
messages[].room_id required string (uuid) The room the message belongs to.
messages[].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.
messages[].client_message_id required string The sender's idempotency key for this message.
messages[].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.
messages[].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.
messages[].reply_to string (uuid) The id of the message this one replies to, in the same room. Absent when it is not a reply.
messages[].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.
messages[].created_at required string (date-time) When the message was committed.
messages[].edited_at string (date-time) When the message was last edited. Absent if never edited.
messages[].deleted_at string (date-time) When the message was soft-deleted. Absent unless deleted.
messages[].attachments array of AttachmentRef The files attached to the message. Absent when there are none.
messages[].attachments[].id required if its parent is sent string (uuid) The attachment's id.
messages[].attachments[].original_name required if its parent is sent string The file name as uploaded (sanitised).
messages[].attachments[].mime required if its parent is sent string The attachment's content type.
messages[].attachments[].byte_size required if its parent is sent integer (int64) Size in bytes.
messages[].attachments[].status required if its parent is sent string pending until the upload completes, uploaded while it is verified, then ready or failed.
messages[].attachments[].width integer Image width in pixels, when known.
messages[].attachments[].height integer Image height in pixels, when known.
messages[].attachments[].has_thumbnail required if its parent is sent boolean Whether a thumbnail was generated; the download response then carries thumbnail_url.
messages[].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.
messages[].reactions[].emoji required if its parent is sent string The emoji reacted with.
messages[].reactions[].count required if its parent is sent integer (int64) How many principals have reacted with it. Always principals.length.
messages[].reactions[].principals required if its parent is sent array of string Who reacted, by principal id.
latest_seq required integer (int64) The room's highest event_seq at query time. Keep it for catch-up after a reconnect.
Errors · 401 · 403 · 429
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
429Rate limit exceeded
PATCH /v1/rooms/{room_id}/messages/{message_id} #

Edit (author only)

editMessage captured live

Request

curl -X PATCH https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/messages/e8b186e4-845a-41dc-bb28-102dd8d0cb8d \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"body":{"text":"Hey — are we still on for 3:30pm?"}}'
{
  "body": {
    "text": "Hey — are we still on for 3:30pm?"
  }
}
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
message_id path required string (uuid) The message's id, in this 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
body required any The new content, replacing the old: any JSON value, at most 16 KiB serialised. Only the sender can edit.
if_version string Optional precondition: the version of the message this edit was composed against, which is its edited_at as an RFC 3339 timestamp, or "0" when it has never been edited. The edit applies only while the message is still on that version; otherwise 409 conflict. Without it an edit is last-writer-wins, which is unsafe for a request that may be resent after a lost response.

Response · 200

{
  "message": {
    "body": {
      "text": "Hey — are we still on for 3:30pm?"
    },
    "client_message_id": "c-9f3b7e21",
    "created_at": "2026-01-15T09:30:06Z",
    "edited_at": "2026-01-15T09:30:06Z",
    "event_seq": 5202,
    "id": "e8b186e4-845a-41dc-bb28-102dd8d0cb8d",
    "kind": "text",
    "room_id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
    "sender_id": "alice"
  }
}
Response fields (26)
FieldTypeDescription
message required Message The message as stored.
message.id required string (uuid) The message's id.
message.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.
message.room_id required string (uuid) The room the message belongs to.
message.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.
message.client_message_id required string The sender's idempotency key for this message.
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.
message.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.
message.reply_to string (uuid) The id of the message this one replies to, in the same room. Absent when it is not a reply.
message.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.
message.created_at required string (date-time) When the message was committed.
message.edited_at string (date-time) When the message was last edited. Absent if never edited.
message.deleted_at string (date-time) When the message was soft-deleted. Absent unless deleted.
message.attachments array of AttachmentRef The files attached to the message. Absent when there are none.
message.attachments[].id required if its parent is sent string (uuid) The attachment's id.
message.attachments[].original_name required if its parent is sent string The file name as uploaded (sanitised).
message.attachments[].mime required if its parent is sent string The attachment's content type.
message.attachments[].byte_size required if its parent is sent integer (int64) Size in bytes.
message.attachments[].status required if its parent is sent string pending until the upload completes, uploaded while it is verified, then ready or failed.
message.attachments[].width integer Image width in pixels, when known.
message.attachments[].height integer Image height in pixels, when known.
message.attachments[].has_thumbnail required if its parent is sent boolean Whether a thumbnail was generated; the download response then carries thumbnail_url.
message.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.
message.reactions[].emoji required if its parent is sent string The emoji reacted with.
message.reactions[].count required if its parent is sent integer (int64) How many principals have reacted with it. Always principals.length.
message.reactions[].principals required if its parent is sent array of string Who reacted, by principal id.
Errors · 401 · 403 · 404 · 409 · 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
409Conflicts with current state
429Rate limit exceeded
DELETE /v1/rooms/{room_id}/messages/{message_id} #

Soft delete

deleteMessage captured live

Request

curl -X DELETE https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/messages/883e28db-e669-4fba-9a60-e071d1b79b8d \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…"
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
message_id path required string (uuid) The message's id, in this 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

{
  "message": {
    "body": null,
    "client_message_id": "c-doomed",
    "created_at": "2026-01-15T09:30:08Z",
    "deleted_at": "2026-01-15T09:30:08Z",
    "event_seq": 5204,
    "id": "883e28db-e669-4fba-9a60-e071d1b79b8d",
    "kind": "deleted",
    "room_id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
    "sender_id": "alice"
  }
}
Response fields (26)
FieldTypeDescription
message required Message The message as stored.
message.id required string (uuid) The message's id.
message.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.
message.room_id required string (uuid) The room the message belongs to.
message.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.
message.client_message_id required string The sender's idempotency key for this message.
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.
message.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.
message.reply_to string (uuid) The id of the message this one replies to, in the same room. Absent when it is not a reply.
message.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.
message.created_at required string (date-time) When the message was committed.
message.edited_at string (date-time) When the message was last edited. Absent if never edited.
message.deleted_at string (date-time) When the message was soft-deleted. Absent unless deleted.
message.attachments array of AttachmentRef The files attached to the message. Absent when there are none.
message.attachments[].id required if its parent is sent string (uuid) The attachment's id.
message.attachments[].original_name required if its parent is sent string The file name as uploaded (sanitised).
message.attachments[].mime required if its parent is sent string The attachment's content type.
message.attachments[].byte_size required if its parent is sent integer (int64) Size in bytes.
message.attachments[].status required if its parent is sent string pending until the upload completes, uploaded while it is verified, then ready or failed.
message.attachments[].width integer Image width in pixels, when known.
message.attachments[].height integer Image height in pixels, when known.
message.attachments[].has_thumbnail required if its parent is sent boolean Whether a thumbnail was generated; the download response then carries thumbnail_url.
message.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.
message.reactions[].emoji required if its parent is sent string The emoji reacted with.
message.reactions[].count required if its parent is sent integer (int64) How many principals have reacted with it. Always principals.length.
message.reactions[].principals required if its parent is sent array of string Who reacted, by principal id.
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
GET /v1/rooms/{room_id}/messages/search #

Full-text search within the room

searchMessages captured live

Request

curl -X GET https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/messages/search?q=invoice&limit=20 \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…"
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
q query required string search terms
limit query integer (int64) Maximum results, 1–100. Defaults to 50.
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

{
  "messages": [
    {
      "id": "5c6e7377-ca7d-4567-9beb-dafb87d2a4d9",
      "event_seq": 5203,
      "room_id": "25964f12-9ae9-4356-80e8-69bc5f749bd1",
      "sender_id": "bob",
      "client_message_id": "c-searchable",
      "kind": "text",
      "body": {
        "text": "The quarterly invoice is attached."
      },
      "created_at": "2026-01-15T09:30:07Z"
    }
  ],
  "latest_seq": 5203
}
Response fields (27)
FieldTypeDescription
messages required array of Message The messages. From history, oldest first (ascending event_seq); from search, best match first, then newest.
messages[].id required string (uuid) The message's id.
messages[].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.
messages[].room_id required string (uuid) The room the message belongs to.
messages[].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.
messages[].client_message_id required string The sender's idempotency key for this message.
messages[].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.
messages[].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.
messages[].reply_to string (uuid) The id of the message this one replies to, in the same room. Absent when it is not a reply.
messages[].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.
messages[].created_at required string (date-time) When the message was committed.
messages[].edited_at string (date-time) When the message was last edited. Absent if never edited.
messages[].deleted_at string (date-time) When the message was soft-deleted. Absent unless deleted.
messages[].attachments array of AttachmentRef The files attached to the message. Absent when there are none.
messages[].attachments[].id required if its parent is sent string (uuid) The attachment's id.
messages[].attachments[].original_name required if its parent is sent string The file name as uploaded (sanitised).
messages[].attachments[].mime required if its parent is sent string The attachment's content type.
messages[].attachments[].byte_size required if its parent is sent integer (int64) Size in bytes.
messages[].attachments[].status required if its parent is sent string pending until the upload completes, uploaded while it is verified, then ready or failed.
messages[].attachments[].width integer Image width in pixels, when known.
messages[].attachments[].height integer Image height in pixels, when known.
messages[].attachments[].has_thumbnail required if its parent is sent boolean Whether a thumbnail was generated; the download response then carries thumbnail_url.
messages[].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.
messages[].reactions[].emoji required if its parent is sent string The emoji reacted with.
messages[].reactions[].count required if its parent is sent integer (int64) How many principals have reacted with it. Always principals.length.
messages[].reactions[].principals required if its parent is sent array of string Who reacted, by principal id.
latest_seq required integer (int64) The room's highest event_seq at query time. Keep it for catch-up after a reconnect.
Errors · 401 · 403 · 409 · 429
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
409room is e2ee; the server cannot search ciphertext
429Rate limit exceeded
POST /v1/rooms/{room_id}/messages/{message_id}/reactions #

Add a reaction

addReaction captured live

Request

curl -X POST https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/messages/e8b186e4-845a-41dc-bb28-102dd8d0cb8d/reactions \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"emoji":"👍"}'
{
  "emoji": "👍"
}
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
message_id path required string (uuid) The message's id, in this 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
emoji required string max length 32 The reaction: 1–8 characters and at most 32 bytes, usually a single emoji. A message holds a bounded number of distinct reactions.

Response · 200

{
  "changed": true
}
Response fields (1)
FieldTypeDescription
changed required boolean false when the call was a no-op, which makes retries safe
Errors · 401 · 403 · 404 · 409 · 429
StatusWhen
401Missing, expired or invalid credentials
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
409room is e2ee
429Rate limit exceeded
DELETE /v1/rooms/{room_id}/messages/{message_id}/reactions #

Remove a reaction

removeReaction captured live

Request

curl -X DELETE https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/messages/e8b186e4-845a-41dc-bb28-102dd8d0cb8d/reactions \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"emoji":"👍"}'
{
  "emoji": "👍"
}
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
message_id path required string (uuid) The message's id, in this 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
emoji required string max length 32 The reaction: 1–8 characters and at most 32 bytes, usually a single emoji. A message holds a bounded number of distinct reactions.

Response · 200

{
  "changed": true
}
Response fields (1)
FieldTypeDescription
changed required boolean false when the call was a no-op, which makes retries safe
Errors · 401 · 403 · 404 · 429
StatusWhen
401Missing, expired or invalid credentials
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
429Rate limit exceeded

Receipts & presence #

POST /v1/rooms/{room_id}/messages/{message_id}/delivered #

Delivery receipt

receiptDelivered captured live

Request

curl -X POST https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/messages/e8b186e4-845a-41dc-bb28-102dd8d0cb8d/delivered \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…"
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
message_id path required string (uuid) The message's id, in this 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

{
  "advanced": true
}
Response fields (1)
FieldTypeDescription
advanced required boolean false when the cursor already covered this message, which makes retries safe
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
POST /v1/rooms/{room_id}/messages/{message_id}/read #

Read receipt (advances cursor)

receiptRead captured live

Request

curl -X POST https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/messages/e8b186e4-845a-41dc-bb28-102dd8d0cb8d/read \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…"
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
message_id path required string (uuid) The message's id, in this 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

{
  "advanced": true
}
Response fields (1)
FieldTypeDescription
advanced required boolean false when the cursor already covered this message, which makes retries safe
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
GET /v1/rooms/{room_id}/unread #

Unread count and cursors

unread captured live

Request

curl -X GET https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/unread \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…"
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
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

{
  "unread_count": 0,
  "last_read_seq": 5202,
  "latest_seq": 5202
}
Response fields (3)
FieldTypeDescription
unread_count required integer (int64) messages after last_read_seq not sent by the caller
last_read_seq required integer (int64) read cursor; advances monotonically and syncs across the devices of a principal
latest_seq required integer (int64) The room's highest event_seq at the time of the request.
Errors · 401 · 403 · 429
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
429Rate limit exceeded
GET /v1/rooms/{room_id}/participants #

Room participants

participants captured live

Request

curl -X GET https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/participants \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…"
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
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

{
  "participants": [
    {
      "principal_id": "alice",
      "role": "owner",
      "online": false
    },
    {
      "principal_id": "bob",
      "role": "member",
      "online": false
    },
    {
      "principal_id": "carol",
      "role": "member",
      "online": false
    },
    {
      "principal_id": "support-bot",
      "role": "member",
      "online": false
    }
  ]
}
Response fields (4)
FieldTypeDescription
participants required array of Participant Every active member of the room, with chat presence. Not who is in a call.
participants[].principal_id required string The member's principal id.
participants[].role required string The member's role: owner, member or viewer.
participants[].online required boolean live presence at query time, from Valkey; absent presence reads as false rather than failing the call
Errors · 401 · 403 · 429
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
429Rate limit exceeded

Attachments #

POST /v1/rooms/{room_id}/attachments/init #

Begin an upload

initAttachment captured live

Request

curl -X POST https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/attachments/init \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"original_name":"invoice-q3.jpg","declared_mime":"image/jpeg","byte_size":160}'
{
  "original_name": "invoice-q3.jpg",
  "declared_mime": "image/jpeg",
  "byte_size": 160
}
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
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
original_name required string max length 255 The file name, 1–255 bytes after trimming. Names containing /, \, a NUL byte or .. are refused.
declared_mime required one of: image/jpeg, image/png, image/webp, image/gif, audio/ogg, audio/mpeg, audio/mp4, video/mp4, video/webm, application/pdf, text/plain, application/zip The file's type. It must be on the allowlist, and the server checks the uploaded bytes match it. In an E2EE room only application/octet-stream (ciphertext) is accepted.
byte_size required integer (int64) min 1 Exact size in bytes, from 1 up to the deployment's upload limit (100 MiB by default). Counts against the tenant's monthly byte quota.
sha256 string Optional SHA-256 of the file, hex. When given, the upload is rejected if the stored bytes do not match it.
poster_of string (uuid) Declare this upload as the poster frame of an existing video attachment in the same room. The upload must be an image and the referenced attachment a video, else 400. The video's download response then carries poster_url.

Response · 200

{
  "attachment_id": "ee86f695-3f58-48e2-9ca8-66ee9651ee3e",
  "upload_url": "https://files.example.com/objects/…?X-Amz-Signature=…",
  "upload_expires_at": "2026-01-15T09:35:10Z",
  "required_headers": [
    [
      "content-type",
      "application/octet-stream"
    ]
  ]
}
Response fields (4)
FieldTypeDescription
attachment_id required string (uuid) The attachment's id. Complete the upload with it, then attach it to a message.
upload_url required string (uri) Presigned PUT URL, 5-minute expiry; upload directly, bytes never pass through the API
upload_expires_at required string (date-time) When upload_url stops accepting the PUT. The window scales with the file size, from 5 minutes up to 2 hours.
required_headers required array of array of any Headers the PUT to upload_url must carry, as [name, value] pairs. The signature covers them, so send each one exactly.
Errors · 401 · 402 · 403 · 413 · 415 · 429
StatusWhen
401Missing, expired or invalid credentials
402This upload would take the tenant past its monthly byte quota (quota_exceeded). Retrying will not help until the quota resets at the start of the month (UTC) or the plan changes.
403Authenticated, but the token is not scoped to this room or principal
413Body exceeds MAX_UPLOAD_BYTES or the per-message limit
415Blocked or unknown type
429Rate limit exceeded
POST /v1/rooms/{room_id}/attachments/{attachment_id}/complete #

Finish and verify

completeAttachment captured live

Request

curl -X POST https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/attachments/ee86f695-3f58-48e2-9ca8-66ee9651ee3e/complete \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{}'
{}
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
attachment_id path required string (uuid) The attachment's id, as returned when the upload began.
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

{
  "attachment_id": "ee86f695-3f58-48e2-9ca8-66ee9651ee3e",
  "status": "uploaded"
}
Response fields (2)
FieldTypeDescription
attachment_id required string (uuid) The attachment's id.
status required one of: uploaded, ready uploaded — accepted and queued for verification (the normal answer); ready — this attachment was already verified, so completing it again is a no-op. Any other state answers 409.
Errors · 401 · 403 · 404 · 409 · 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
409Not uploaded / size mismatch
429Rate limit exceeded
POST /v1/rooms/{room_id}/attachments/init-multipart #

Begin a multipart upload

initMultipart captured live

Request

curl -X POST https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/attachments/init-multipart \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"original_name":"standup.mp4","declared_mime":"video/mp4","byte_size":5243904,"part_size":5242880}'
{
  "original_name": "standup.mp4",
  "declared_mime": "video/mp4",
  "byte_size": 5243904,
  "part_size": 5242880
}
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
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
original_name required string The file name, 1–255 bytes after trimming. Names containing /, \, a NUL byte or .. are refused.
declared_mime required string verified against magic bytes after upload; a mismatch fails the attachment
byte_size required integer (int64) Exact size in bytes. Multipart is for files larger than 5 MiB, up to the deployment's upload limit (100 MiB by default); smaller files use the single-part upload.
part_size integer (int64) optional; server chooses a default when omitted

Response · 200

{
  "attachment_id": "2b575b88-88bb-40d0-b616-57536b782806",
  "upload_id": "ZTY5ODdmMjMtMGI1OS00Y2QxLTk5MzYtNzllZjk2YjdiZDY5LmZkZGEzZGE4LTJmYjUtNDJjZS1iY2E2LTk4ZDhiZjM3NGJkOXgxNzkyMzI0Njc1MTk1NzY5NDk1",
  "part_size": 5242880,
  "part_urls": [
    "https://files.example.com/objects/…?X-Amz-Signature=…",
    "https://files.example.com/objects/…?X-Amz-Signature=…"
  ],
  "expires_at": "2026-01-15T09:35:12Z"
}
Response fields (5)
FieldTypeDescription
attachment_id required string (uuid) The attachment's id. Use it to complete the upload and to attach the file to a message.
upload_id required string The object store's id for this multipart upload. Opaque; the complete call does not need it.
part_size required integer (int64) Bytes per part: 16 MiB unless you asked for another size, clamped to 5–64 MiB. Split the file at this size; only the last part may be smaller.
part_urls required array of string (uri) one presigned PUT per part, in order
expires_at required string (date-time) When the part URLs stop accepting uploads.
Errors · 401 · 402 · 403 · 413 · 415 · 429
StatusWhen
401Missing, expired or invalid credentials
402This upload would take the tenant past its monthly byte quota (quota_exceeded). Retrying will not help until the quota resets at the start of the month (UTC) or the plan changes.
403Authenticated, but the token is not scoped to this room or principal
413declared size exceeds the configured limit
415declared type not permitted
429Rate limit exceeded
POST /v1/rooms/{room_id}/attachments/{attachment_id}/complete-multipart #

Finish multipart

completeMultipart captured live

Request

curl -X POST https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/attachments/2b575b88-88bb-40d0-b616-57536b782806/complete-multipart \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"parts":[{"part_number":1,"etag":"b8fc857a25e7958868c2f003d5e0952d"},{"part_number":2,"etag":"e9f8000caffbce369d7fee9c07d43509"}]}'
{
  "parts": [
    {
      "part_number": 1,
      "etag": "b8fc857a25e7958868c2f003d5e0952d"
    },
    {
      "part_number": 2,
      "etag": "e9f8000caffbce369d7fee9c07d43509"
    }
  ]
}
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
attachment_id path required string (uuid) The attachment's id, as returned when the upload began.
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
parts required array of MultipartPart Every part, with the ETag header the object store returned for its PUT, in part-number order.
parts[].part_number required integer 1-based
parts[].etag required string ETag returned by the presigned PUT for this part

Response · 200

{
  "attachment_id": "2b575b88-88bb-40d0-b616-57536b782806",
  "status": "uploaded"
}
Response fields (2)
FieldTypeDescription
attachment_id required string (uuid) The attachment's id.
status required one of: uploaded Always uploaded — accepted and queued for verification. Unlike the single-part complete there is no idempotent no-op: completing again once the upload has left pending answers 409.
Errors · 400 · 401 · 403 · 404 · 429
StatusWhen
400part list does not match the upload
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
GET /v1/rooms/{room_id}/attachments/{attachment_id}/download #

Presigned download

downloadAttachment captured live

Request

curl -X GET https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/attachments/ee86f695-3f58-48e2-9ca8-66ee9651ee3e/download \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…"
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
attachment_id path required string (uuid) The attachment's id, as returned when the upload began.
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

{
  "download_url": "https://files.example.com/objects/…?X-Amz-Signature=…",
  "expires_at": "2026-01-15T09:35:12Z",
  "mime": "image/jpeg",
  "original_name": "invoice-q3.jpg",
  "thumbnail_url": "https://files.example.com/objects/…?X-Amz-Signature=…",
  "disposition": "inline"
}
Response fields (7)
FieldTypeDescription
download_url required string (uri) A presigned GET URL for the file.
expires_at required string (date-time) When download_url (and thumbnail_url) stop working. Ask again for a fresh one.
mime required string The file's content type.
original_name required string The file name as uploaded (sanitised).
thumbnail_url string (uri) Presigned URL of the generated thumbnail. Absent when none exists.
poster_url string (uri) Presigned URL of the video's poster frame, when one was uploaded with poster_of. Absent otherwise.
disposition required one of: inline, attachment How the URL serves the file: inline for images, audio and video; attachment (a download) for everything else, such as PDFs, text and zip files.
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

Push #

POST /v1/push/subscriptions #

Register a Web Push subscription

pushSubscribe captured live

Request

curl -X POST https://api.ollacore.com/v1/push/subscriptions \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"endpoint":"https://fcm.googleapis.com/fcm/send/EXAMPLE","keys":{"p256dh":"<redacted>","auth":"c2VjcmV0MTIzNDU2Nzg"},"user_agent":"Mozilla/5.0 (Android 14; Pixel 8)"}'
{
  "endpoint": "https://fcm.googleapis.com/fcm/send/EXAMPLE",
  "keys": {
    "p256dh": "<redacted>",
    "auth": "c2VjcmV0MTIzNDU2Nzg"
  },
  "user_agent": "Mozilla/5.0 (Android 14; Pixel 8)"
}
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
endpoint required string (uri) Push service endpoint from PushSubscription.toJSON(). Must be https with a public host: cleartext, loopback, private and link-local IP literals are rejected with 400. A hostname is resolved at each delivery; if it resolves to such an address the server does not connect and drops the subscription.
keys required PushKeys The encryption keys from PushSubscription.toJSON().
keys.p256dh required string The subscription's public encryption key, exactly as PushSubscription.toJSON() returns it (base64url).
keys.auth required string The subscription's authentication secret, exactly as PushSubscription.toJSON() returns it (base64url).
user_agent string Optional free-text label for the device, stored with the subscription.

Response · 200

{
  "subscribed": true
}
Response fields (1)
FieldTypeDescription
subscribed required boolean Always true on success.
Errors · 401 · 429
StatusWhen
401Missing, expired or invalid credentials
429Rate limit exceeded
DELETE /v1/push/subscriptions #

Unregister one

pushUnsubscribe captured live

Request

curl -X DELETE https://api.ollacore.com/v1/push/subscriptions \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"endpoint":"https://fcm.googleapis.com/fcm/send/EXAMPLE"}'
{
  "endpoint": "https://fcm.googleapis.com/fcm/send/EXAMPLE"
}
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
endpoint required string (uri) The subscription's endpoint URL. Only your own subscription is removed; the call works even after you leave the room, while your token is still valid.

Response · 200

{
  "removed": true
}
Response fields (1)
FieldTypeDescription
removed required boolean True if a subscription owned by the caller matched the endpoint and was deleted; false if there was nothing to delete.
Errors · 401 · 429
StatusWhen
401Missing, expired or invalid credentials
429Rate limit exceeded
GET /v1/push/vapid-public-key #

VAPID public key

vapidPublicKey captured live

Request

curl -X GET https://api.ollacore.com/v1/push/vapid-public-key \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…"
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

{
  "vapid_public_key": "BOCW_h9I_7rKsBL9E6EXl2eENH5vPkbuYPcIR9AVhUDwFJipZ4vC5RMAUqiKWSlVEhK3T2Zyv0DnJGXC1i0cNoE"
}
Response fields (1)
FieldTypeDescription
vapid_public_key required string The server's VAPID public key. Pass it as applicationServerKey to pushManager.subscribe.
Errors · 404
StatusWhen
404push not configured on this deployment

End-to-end encryption #

POST /v1/e2ee/keys/upload #

Publish device + one-time keys

keysUpload captured live

Request

curl -X POST https://api.ollacore.com/v1/e2ee/keys/upload \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"device_keys":{"user_id":"alice","device_id":"dev-alice","algorithms":["m.olm.v1.curve25519-aes-sha2","m.megolm.v1.aes-sha2"],"keys":{"curve25519:dev-alice":"JGDn8Xz3…","ed25519:dev-alice":"lEuiSJHm…"}},"one_time_keys":{"signed_curve25519:AAAAAQ":{"key":"zKbLg+NrIjpn…"},"signed_curve25519:AAAAAg":{"key":"mYWt2yTv8p…"},"signed_curve25519:AAAAAw":{"key":"qR7dLm3nXk…"}}}'
{
  "device_keys": {
    "user_id": "alice",
    "device_id": "dev-alice",
    "algorithms": [
      "m.olm.v1.curve25519-aes-sha2",
      "m.megolm.v1.aes-sha2"
    ],
    "keys": {
      "curve25519:dev-alice": "JGDn8Xz3…",
      "ed25519:dev-alice": "lEuiSJHm…"
    }
  },
  "one_time_keys": {
    "signed_curve25519:AAAAAQ": {
      "key": "zKbLg+NrIjpn…"
    },
    "signed_curve25519:AAAAAg": {
      "key": "mYWt2yTv8p…"
    },
    "signed_curve25519:AAAAAw": {
      "key": "qR7dLm3nXk…"
    }
  }
}
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
device_keys object The device's identity keys, in the Matrix /keys/upload shape. At most 64 KiB.
one_time_keys object One-time keys to add, keyed <algorithm>:<key id> (Matrix shape). At most 100 per upload.
fallback_keys object Fallback keys, served when the one-time keys run out (Matrix shape).

Response · 200

{
  "one_time_key_counts": {
    "signed_curve25519": 3
  }
}
Response fields (2)
FieldTypeDescription
one_time_key_counts required OneTimeKeyCounts The device's remaining one-time keys after this upload, per algorithm.
one_time_key_counts.signed_curve25519 required integer (int64) How many unclaimed one-time keys the device has left on the server. Upload more before it runs low.
Errors · 400 · 401 · 403 · 429
StatusWhen
400missing did claim / oversized blob
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
429Rate limit exceeded
POST /v1/e2ee/keys/query #

Query devices for principals

keysQuery captured live

Request

curl -X POST https://api.ollacore.com/v1/e2ee/keys/query \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"device_keys":{"alice":{},"bob":{}}}'
{
  "device_keys": {
    "alice": {},
    "bob": {}
  }
}
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
device_keys required object principal -> {} (device filter ignored; all devices returned)

Response · 200

{
  "device_keys": {
    "alice": {
      "dev-alice": {
        "algorithms": [
          "m.olm.v1.curve25519-aes-sha2",
          "m.megolm.v1.aes-sha2"
        ],
        "device_id": "dev-alice",
        "keys": {
          "curve25519:dev-alice": "JGDn8Xz3…",
          "ed25519:dev-alice": "lEuiSJHm…"
        },
        "user_id": "alice"
      }
    },
    "bob": {
      "dev-bob": {
        "algorithms": [
          "m.olm.v1.curve25519-aes-sha2"
        ],
        "device_id": "dev-bob",
        "keys": {
          "curve25519:dev-bob": "aBcD…"
        },
        "user_id": "bob"
      }
    }
  }
}
Response fields (1)
FieldTypeDescription
device_keys required object principal id -> device id -> the device key object that principal published
Errors · 401 · 403 · 429
StatusWhen
401Missing, expired or invalid credentials
403no shared room
429Rate limit exceeded
POST /v1/e2ee/keys/claim #

Claim a one-time key (atomic)

keysClaim captured live

Request

curl -X POST https://api.ollacore.com/v1/e2ee/keys/claim \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"one_time_keys":{"bob":{"dev-bob":"signed_curve25519"}}}'
{
  "one_time_keys": {
    "bob": {
      "dev-bob": "signed_curve25519"
    }
  }
}
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
one_time_keys required object principal -> device -> algorithm

Response · 200

{
  "one_time_keys": {
    "bob": {
      "dev-bob": {
        "signed_curve25519:BBBBAQ": {
          "key": "nnn…"
        }
      }
    }
  }
}
Response fields (1)
FieldTypeDescription
one_time_keys required object principal id -> device id -> { key_id: key }; a device is absent when its pool is dry and it published no fallback key
Errors · 401 · 403 · 429
StatusWhen
401Missing, expired or invalid credentials
403no shared room
429Rate limit exceeded
POST /v1/e2ee/todevice #

Send a to-device message

todeviceSend captured live

Request

curl -X POST https://api.ollacore.com/v1/e2ee/todevice \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"event_type":"m.room_key_request","messages":{"bob":{"dev-bob":{"action":"request","requesting_device_id":"dev-alice"}}}}'
{
  "event_type": "m.room_key_request",
  "messages": {
    "bob": {
      "dev-bob": {
        "action": "request",
        "requesting_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
event_type required string The to-device event type, 1–128 characters, for example m.room.encrypted.
messages required object principal -> device|* -> content

Response · 200

{
  "queued": 1
}
Response fields (1)
FieldTypeDescription
queued required integer (int64) per-device rows written; a wildcard recipient fans out to every known device
Errors · 401 · 403 · 429
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
429Rate limit exceeded
GET /v1/e2ee/todevice #

Fetch queued to-device messages

todeviceFetch captured live

Request

curl -X GET https://api.ollacore.com/v1/e2ee/todevice \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…"
ParameterTypeDescription
ack query integer (int64) default 0 next_token of the last fully processed batch
device_watermark query integer (int64) default 0 The device_list_watermark from your previous fetch; 0 (the default) on the first.
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

{
  "device_list_changes": [
    "alice",
    "bob"
  ],
  "device_list_watermark": 157,
  "events": [
    {
      "content": {
        "action": "request",
        "requesting_device_id": "dev-alice"
      },
      "sender": "alice",
      "type": "m.room_key_request"
    }
  ],
  "next_token": 130,
  "one_time_key_counts": {
    "signed_curve25519": 1
  },
  "unused_fallback_key_algorithms": []
}
Response fields (10)
FieldTypeDescription
events required array of ToDeviceEvent Queued to-device events for this device, oldest first. Acknowledge them on the next fetch so they are deleted.
events[].sender required string server-stamped from the token, never taken from the request body
events[].type required string The event type the sender used, for example m.room.encrypted.
events[].content required object The event content as the sender wrote it. Opaque to the server.
next_token required integer (int64) pass back as ack once the batch is processed; an unacked batch is re-served
one_time_key_counts required OneTimeKeyCounts The device's remaining one-time keys, per algorithm.
one_time_key_counts.signed_curve25519 required integer (int64) How many unclaimed one-time keys the device has left on the server. Upload more before it runs low.
unused_fallback_key_algorithms required array of string Algorithms whose fallback key has not been used yet. Empty means upload a new fallback key.
device_list_changes required array of string principals whose device list changed since device_watermark
device_list_watermark required integer (int64) Pass this back as device_watermark on the next fetch to learn which users' device lists changed since.
Errors · 401 · 429
StatusWhen
401Missing, expired or invalid credentials
429Rate limit exceeded
POST /v1/keypackages #

Upload MLS key packages

uploadKeyPackages captured live

Request

curl -X POST https://api.ollacore.com/v1/keypackages \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"device_id":"dev-alice","packages":["AAECAwQFBgcICQoLDA0ODw==","EBESExQVFhcYGRobHB0eHw==","ICEiIyQlJicoKSorLC0uLw=="]}'
{
  "device_id": "dev-alice",
  "packages": [
    "AAECAwQFBgcICQoLDA0ODw==",
    "EBESExQVFhcYGRobHB0eHw==",
    "ICEiIyQlJicoKSorLC0uLw=="
  ]
}
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
device_id string The device these key packages belong to. Defaults to default.
packages required array of string opaque base64 MLS key packages; never interpreted server-side
last_resort boolean default false true publishes the device's single last-resort package (RFC 9750): it is served when the device's regular pool is dry and is never consumed by being served. The request must carry exactly one package; a new last-resort upload replaces the previous one.
expires_in_seconds integer min 1 · max 31536000 Server-side lifetime. Past it the package is never handed out and is swept. Omit to keep the package until it is consumed.

Response · 200

{
  "stored": 3
}
Response fields (1)
FieldTypeDescription
stored required integer How many key packages were stored.
Errors · 401 · 413 · 429
StatusWhen
401Missing, expired or invalid credentials
413Body exceeds MAX_UPLOAD_BYTES or the per-message limit
429Rate limit exceeded
GET /v1/keypackages/count #

Remaining key-package count

keyPackageCount captured live

Request

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

{
  "devices": [
    {
      "device_id": "dev-alice",
      "last_resort": false,
      "remaining": 3
    }
  ]
}
Response fields (4)
FieldTypeDescription
devices required array of object Your devices that have key packages on the server.
devices[].device_id required string The device.
devices[].remaining required integer How many unconsumed, unexpired regular key packages it has left. The last-resort package is not counted here.
devices[].last_resort boolean Whether an unexpired last-resort package is on file for this device. Always present.
Errors · 401 · 429
StatusWhen
401Missing, expired or invalid credentials
429Rate limit exceeded
GET /v1/rooms/{room_id}/keypackages/{principal_id} #

Consume one key package

consumeKeyPackages captured live

Request

curl -X GET https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/keypackages/alice \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…"
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
principal_id path required string Whose key package to consume. They must share an active room with the caller.
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",
  "packages": [
    {
      "device_id": "dev-alice",
      "package": "AAECAwQFBgcICQoLDA0ODw==",
      "last_resort": false
    }
  ]
}
Response fields (5)
FieldTypeDescription
principal_id required string Whose key packages these are.
packages required array of ConsumedKeyPackage At most one package per device of the principal, oldest first. A regular package is handed out once: it is marked consumed and never served again. When a device's regular pool is dry its last-resort package is served instead, unconsumed, and flagged last_resort: true. Expired packages are never served.
packages[].device_id required string The device this key package belongs to.
packages[].package required string The MLS key package, base64-encoded. Opaque to the server.
packages[].last_resort boolean true when this is the device's last-resort package, served because its regular pool was dry. Other joiners may have received the same one; expect the owner to replace it. Always present.
Errors · 401 · 403 · 404 · 429
StatusWhen
401Missing, expired or invalid credentials
403no shared active room with the target
404No such room, message, member or attachment
429Rate limit exceeded
PUT /v1/rooms/{room_id}/mls/group-info #

Publish the MLS GroupInfo + ratchet tree

putGroupInfo captured live

Request

curl -X PUT https://api.ollacore.com/v1/rooms/f10b8ba1-6206-4994-9e66-597cef0a6792/mls/group-info \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…" \
  -H "Content-Type: application/json" \
  -d '{"epoch":3,"group_info":"AAECAwQFBgcICQoLDA0ODw==","ratchet_tree":"EBESExQVFhcYGRobHB0eHw=="}'
{
  "epoch": 3,
  "group_info": "AAECAwQFBgcICQoLDA0ODw==",
  "ratchet_tree": "EBESExQVFhcYGRobHB0eHw=="
}
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
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
epoch required integer (int64) min 0 The MLS epoch this GroupInfo describes. An epoch older than the one already published is refused with 409; the same epoch may be re-published.
group_info required string base64 TLS-serialized GroupInfo (an MLSMessage of type group_info). Opaque to the server; at most 64 KiB.
ratchet_tree string base64 TLS-serialized ratchet tree, so Welcomes can omit the ratchet_tree extension. Opaque; at most 256 KiB — about a hundred members on a post-quantum cipher suite.

Response · 200

{
  "epoch": 3
}
Response fields (1)
FieldTypeDescription
epoch required integer (int64) The epoch now published.
Errors · 401 · 403 · 409 · 413 · 429
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
409Conflicts with current state
413Body exceeds MAX_UPLOAD_BYTES or the per-message limit
429Rate limit exceeded
GET /v1/rooms/{room_id}/mls/group-info #

Latest GroupInfo + ratchet tree

getGroupInfo captured live

Request

curl -X GET https://api.ollacore.com/v1/rooms/f10b8ba1-6206-4994-9e66-597cef0a6792/mls/group-info \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…"
ParameterTypeDescription
room_id path required string (uuid) The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
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

{
  "epoch": 3,
  "group_info": "AAECAwQFBgcICQoLDA0ODw==",
  "ratchet_tree": "EBESExQVFhcYGRobHB0eHw==",
  "updated_by": "alice",
  "updated_at": "2026-01-15T09:30:53Z"
}
Response fields (5)
FieldTypeDescription
epoch required integer (int64) The epoch the published state describes.
group_info required string base64 TLS-serialized GroupInfo, exactly as published.
ratchet_tree string base64 TLS-serialized ratchet tree, when the committer published one.
updated_by required string The principal who published it.
updated_at required string (date-time)
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

Deprecated #

GET /v1/rooms/{room_id}/recordings #

Sunset 2027-01-05 — camera feeds were withdrawn; always an empty list, with Deprecation and Sunset headers

listRecordings captured live

Request

curl -X GET https://api.ollacore.com/v1/rooms/25964f12-9ae9-4356-80e8-69bc5f749bd1/recordings?limit=20 \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSJ9…"
ParameterTypeDescription
room_id path required string (uuid) The camera's room id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member.
from query string (date-time) Only segments starting at or after this instant.
to query string (date-time) Only segments starting before this instant.
limit query integer default 100 · min 1 · max 500 Maximum segments, counted back from the newest.
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

{
  "segments": []
}
Response fields (5)
FieldTypeDescription
segments required array of RecordingSegment Newest first. Empty when the camera has recorded nothing in the window, and on a deployment with no recording store.
segments[].started_at required string (date-time) When this segment's first frame was recorded. Segments run up to a minute; a gap between one segment's start and the next means the camera was not publishing.
segments[].bytes required integer (int64) The stored size of the segment, which is what it counts against the tenant’s recording storage.
segments[].download_url required string A signed link to the fragmented-MP4 segment, valid for 5 minutes. Fetch the list again for a fresh one rather than storing these.
segments[].expires_at required string (date-time) When download_url stops working.
Errors · 401 · 403 · 429
StatusWhen
401Missing, expired or invalid credentials
403Authenticated, but the token is not scoped to this room or principal
429Rate limit exceeded
Deprecated since 2026-10-03 (camera feeds were decommissioned): always an empty list, with Deprecation and Sunset headers on every response. May be removed on or after 2027-01-05.

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

401 · malformed bearer token

{
  "code": "unauthorized",
  "message": "unauthorized: invalid token: malformed"
}

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