Reference

Changelog

Changes you can see from a client, newest first. Each entry that needs something from you says what. Internal work, and anything not yet running in production, is not listed.

/v1 only grows: new fields and endpoints are added, nothing is removed without 90 days' notice through Deprecation and Sunset headers. Build clients that ignore fields they do not know. Labels: Added Changed (behaviour you may need to handle) Fixed Security.

#

  • Added
    A member's media policy now also decides what it receives and sees: subscribe (all, none, or an allowlist of up to 64 publishers) is enforced inside the SFU on every packet, and roster (all, authorized-only, none) filters peers / peer.joined / peer.left per recipient. A change stops or starts forwarding, re-offers the connection and re-sends peers within about a second. API reference.
    What to do: Nothing for unrestricted members. A restricted member's client must accept re-offers that remove or add sections.
  • Added
    Device limits: "devices": {"max_active": 1..16, "on_limit": "allow" | "reject-new" | "replace-old"} on a member's policy. A device is the session token's device_id. At the limit reject-new sends an error event device_limit and closes the newcomer 4411; replace-old closes the oldest device 4412 policy_changed; the same device_id reconnecting replaces its predecessor (4412 device_reconnected). A lowered limit applies to live devices. Members without a devices section behave as before.
    What to do: Mint each device's tokens with a stable device_id, and treat 4411 and 4412 as "stop, do not reconnect".
  • Added
    Webhook events rtc.device_conflict and rtc.policy.denied report what a member's policy did. They go to your webhook endpoint only, never to the room's sockets. GET /v1/server/rooms/{room_id}/rtc/forwarding reads who receives whose media right now, with the simulcast layer.
    What to do: An endpoint receives these events only if its event_types list them; endpoints cannot be edited, so create one that does.

#

  • Added
    Per-member media policy: PUT / GET / DELETE /v1/server/rooms/{room_id}/members/{principal_id}/rtc-policy. A policy only restricts — effective rights are token ∩ the member's current role ∩ policy — and reaches live connections within about a second. A refused publishing section is answered inactive with an error event beside the answer, and a publication the policy withdraws must be re-offered without it within 5 s or the connection is closed 4409 policy_apply_failed. chat and attachments sections refuse HTTP calls with chat_send_forbidden, chat_history_forbidden, attachment_upload_forbidden or attachment_download_forbidden, and withdrawing history closes the chat socket 4410 policy_changed. Revisions are yours and monotonic; DELETE takes its own ?revision= and leaves a tombstone. API reference.
    What to do: Nothing unless you set a policy. If you do: handle 4409 (reconnect) and 4410 (do not reconnect until the policy changes), and the RTC error event.
  • Added
    A room can declare which end-to-end encryption scheme its members run: "e2ee_scheme": "mls" or "megolm" on room creation (with e2ee: true; immutable), echoed on every room response. The two schemes cannot share a room, and this is how a client knows which engine to load before it touches a message. Undeclared rooms keep their meaning and show no field. End-to-end encryption.
    What to do: Nothing. Declare it on new encrypted rooms so your clients can rely on it.
  • Added
    MLS group state lives on the server: after each commit the committer PUTs /v1/rooms/{room_id}/mls/group-info with the GroupInfo and, optionally, the ratchet tree (base64, opaque; 64 KiB / 256 KiB); a joiner GETs it to obtain the tree instead of receiving it inside the Welcome. An epoch older than the one published is refused with 409; a plaintext room is 409. This is what keeps a Welcome small at any group size — a post-quantum ratchet tree is about 1.3 KB per node. End-to-end encryption.
    What to do: MLS clients: publish after every commit and build Welcomes without the ratchet_tree extension.

#

  • Added
    MLS key packages follow the RFC 9750 directory rules. A device may publish one last-resort package ("last_resort": true, exactly one package per request; a new upload replaces it): when the device's regular pool is dry it is served flagged last_resort: true and not consumed, so a join never fails for want of a package. A package may carry expires_in_seconds (up to a year); expired packages are never served. GET /v1/keypackages/count now reports remaining regular packages and whether a last_resort one is on file. End-to-end encryption.
    What to do: Keep a last-resort package published per device and replace it once remaining reaches 0 — it may have been handed to several joiners by then.
  • Documented
    Stated where it was ambiguous: MLS (RFC 9420) is the primary end-to-end encryption scheme; the server is its Delivery Service. The Megolm/Olm transport (/v1/e2ee/*) stays supported for clients that want the ready-made matrix-sdk-crypto packages. Nothing was renamed or removed.

#

  • Documented
    The automatic layer climb is now written down in numbers, because one integrator lost a day to it. Each subscriber's transport-cc feedback is folded into one-second windows: a window under 2 % loss is clean, three clean windows move the subscriber up a layer, the move is kept once it survives five windows and abandoned on two consecutive windows at 5 % or more, later attempts wait eight clean windows (doubling after a failure, to a maximum of 120), and two windows at 10 % or more drop a layer. A layer must have been measured over two full windows before it can be a target. Calls.
    What to do: Nothing changed in the server. But check that your client keeps the transport-wide-cc header extension on every transceiver it receives on — see the next line.
  • Documented
    Stated explicitly: a client that does not negotiate the transport-wide-cc header extension sends us no feedback, so that viewer stays on the cheapest layer for the entire call — with no error, no warning and nothing in any log. It is the first thing to check when quality never improves on a good connection. A layer your server pins is unaffected.
    What to do: If your client strips or disables that extension anywhere, including on receive-only transceivers, re-enable it.

#

  • Fixed
    A simulcast video track paused by setting its m-section inactive and resumed later on the same mid (sendonly again, same RIDs, a replaceTrack) is forwarded again, within about a second of its first resumed packet. Before, the SFU dropped every packet of the resumed section — the browser keeps the sender's SSRCs across the pause, and the forward table had forgotten them — so subscribers held a freshly re-offered track that never unmuted; and a second pause on the same mid was answered recvonly for an inactive offer, which browsers refuse. Audio and single-encoding video were never affected. Calls.
    What to do: Nothing to change: pause and resume on the same m-section, or a new m-section per restart with the old one left inactive, both work. Expect a new ontrack on each resume.

#

  • Added
    An ephemeral broadcast frame on the chat socket: {"type":"broadcast","payload":{"body":{…},"to"?:"<principal_id>"}} fans body out, unchanged, to the members connected at that moment — or to one member's sockets with to — as a broadcast event with event_id: 0 and payload {from, body, to?}. Nothing is stored, catchup never replays it, and your own sockets receive it too. body is at most 1 KiB as compact JSON; 20 a second per socket, a burst of 40 absorbed. No ack on success; a refusal (payload_too_large, rate_limited with the seconds to wait, blocked, forbidden, invalid_request) is an error ack and the socket stays open. For cursors, pointers, reactions in a call, timers.
    What to do: Nothing, unless you want it. If you have been sending cursors or pointers as messages, send them as broadcasts and they stop filling history (Chat).
  • Added
    Per-subscriber simulcast layer preference, set by your server: PUT /v1/server/rooms/{room_id}/members/{principal_id}/preferences (scope members:write) with {"tracks":[{"publisher":"<principal_id>","track_id":"<publisher's track id>","layer":"low"|"medium"|"high"|"auto"}]}, and GET (scope rooms:read) to read it back with in_call. Keyed by what the member's client already sees — the msid a chatbox.msid-principal socket reads. Names resolve against the publisher's live layers by measured bitrate; a pinned layer that stops is replaced by the dearest live one below and taken back when it returns. Applied within about a second to every connection the member has; held for the session and across a PeerConnection rebuild (30 s grace); the whole list is replaced on each PUT, [] clears. Details: Calls.
    What to do: Needs members:write on the key for PUT and rooms:read for GET. The client needs no change.
  • Fixed
    A simulcast publisher no longer goes silent for every subscriber the first time its connection is renegotiated — in practice, the moment a second publisher joined. The SFU forgot the per-layer SSRCs it had learnt from the first packets, and browsers stop repeating the RID header extension once they see RTCP feedback. Single-encoding publishers were never affected.

#

  • Changed
    A release restarts production once. It used to cut clients twice, about a minute apart. On a deploy every open socket, chat and RTC alike, closes with 1001 server shutting down within milliseconds of the signal, and the service answers again within a few seconds; a request in the gap gets 502 from the edge. There is no reconnect rate limit on either socket. Written down in Deploys.
    What to do: Reconnect on 1001 with exponential backoff and jitter from about one second, then catchup. On the RTC socket, rebuild the PeerConnection: the SFU's state does not survive a restart, so the old connection has credentials and mids the new instance never saw.
  • Changed
    The SFU no longer registers H.265/HEVC. Chrome and Edge offer it only on machines with hardware HEVC support, which gave two browsers of the same version different negotiated codec sets and made the SFU refuse whole tracks. Offer it if you like; it is left out of the answer. VP8, VP9, H.264 and AV1 are negotiated.
    What to do: Nothing, unless your client required H.265.
  • Fixed
    A forwarded track is offered to a subscriber with the codecs the publisher and the subscriber both negotiated. A publisher codec you did not negotiate is left out instead of blocking the whole track; only a track with no codec in common is withheld.
  • Added
    Mentions and forwarding. A message.send (and both REST sends) may carry "mentions": [<principal_id>, …] inside body — at most 50, each an active member, checked on send and stored as sent. A mentioned member is pushed even while they have muted the room. A message is forwarded by sending {"client_message_id": …, "forward_of": "<message id>"} and nothing else: the server copies the source's kind, body and media (as ready attachments of this room) from any room you are in, the copy carries forward_depth, and the source is never named. Readable kinds only; not a deleted message; not into an E2EE room (409). Chat.
    What to do: Render "Forwarded" when forward_depth is present, and @names from mentions.
  • Added
    Directory groups gain a description and an icon_attachment_id (PATCH /v1/directory/conversations/{room_id}, owner only; each change is posted into the room as a system message), any number of owners (PUT …/members/{user_id} with {"role": "owner" | "member"}, announced as member.updated), and invite links: owners create, list and revoke them under …/{room_id}/invites; anyone in the tenant previews and joins with POST /v1/directory/invites/preview and /join, token in the body. Turnkey identity.

#

  • Added
    A directory profile: an about line (up to 256 characters, via PATCH /v1/directory/me) and a photo (POST /v1/directory/me/avatar/init, PUT the bytes, POST …/avatar/complete; DELETE /v1/directory/me/avatar). Every list that names a user — contacts, members, the inbox peer — carries about and avatar_version, never a URL; GET /v1/directory/users/{user_id}/avatar?size=thumb|full is a 302 to the bytes with ETag = avatar_version. Turnkey identity.
    What to do: Cache photo bytes by avatar_version and send If-None-Match to get a 304 instead of a redirect.
  • Added
    RTC subprotocol chatbox.msid-principal: list it ahead of chatbox when opening the RTC socket and, when the server echoes it as the selected subprotocol, every forwarded track's msid is <publisher principal_id> <publisher's own track id>, so event.streams[0].id is the verified principal in every browser. Optional and per socket; a socket that offers only chatbox keeps the peer-<client_id>-… form. Calls.
    What to do: Optional. Check ws.protocol once open before relying on it.

#

  • Changed
    A room holds at most 5,000 active members, on every plane. The add that would exceed it is refused with 409 and nothing changes; restoring a removed member or changing a role never needs a seat. Directory groups are created with the same cap.
    What to do: Handle 409 on member adds.
  • Added
    Directory group management for your users: GET /v1/directory/conversations/{room_id} and …/members for any member; owners add up to 256 members per call (POST …/members), remove one (DELETE …/members/{user_id}); anyone leaves with POST …/leave — a lone owner's seat passes to the longest-standing member, and an empty group closes. Changes arrive as member.added / member.removed.
  • Added
    Mute. A user silences a conversation's push for eight hours, a week or until unmuted: PUT /v1/directory/conversations/{room_id}/mute with {"duration": "8h" | "1w" | "always"}, DELETE to unmute; the inbox shows muted_until. Your server does the same for a member with PUT/DELETE /v1/server/rooms/{room_id}/members/{principal_id}/mute (scope members:write). Push only: the socket still delivers and a call still rings. Private — no event is emitted.
  • Added
    Block. GET /v1/directory/blocks, PUT/DELETE /v1/directory/blocks/{user_id}. A block freezes the direct conversation between the two for both: sends, reactions, typing and the RTC join answer blocked (403), the history stays readable, and a new direct conversation between them is refused the same way. The blocked user cannot add the blocker to a group (not_added, reason blocked); shared groups are unaffected; the blocked person is not told. Turnkey identity.
    What to do: Handle the blocked error code on both planes: show "you blocked this contact" on the blocker's side, and treat it as final rather than retrying.

#

  • Fixed
    Two first-time read receipts for one message from the same member, in flight together, could deadlock on the member row; one of them then answered 500. One member's receipt writes in a room now serialise, and every concurrent read answers 200.

#

  • Changed
    Scheduled retention is resolved from the tenant itself first, then from the plan of the tenant's keys, so an operator can set a window for one tenant without touching the plan. The default is unchanged: keep forever until an operator opts in.

#

  • Changed
    Camera feeds were withdrawn before leaving preview: WHIP and WHEP publishing and viewing, HLS and cloud recording were built and tested on our test deployment and never switched on at api.ollacore.com; the media sidecar has been removed. The one route that reached production, GET /v1/rooms/{room_id}/recordings, is deprecated under the /v1 policy: it answers an empty list, every response carries Deprecation and Sunset headers, and it may be removed on or after 2027-01-05. What remains.
    What to do: Stop calling GET /v1/rooms/{room_id}/recordings; nothing it could return exists.

#

  • Added
    The RTC socket sends roster frames, discriminated by event: peers once on connect, before any SDP, listing everyone already in the call except you; peer.joined (an upsert) and peer.left, each with client_id and principal_id. Calls.
    What to do: client_id is a decimal string that exceeds 2^53 — compare it as a string, never as a number. peer.left is a roster hint, not a media event: do not tear down tracks on it. Ignore event values you do not recognise.
  • Fixed
    A stale tab leaving no longer cancels the ring of a call that is still live.
  • Fixed
    A momentarily full send queue on the RTC signalling socket drops that one frame instead of ending the call.
  • Fixed
    The SFU no longer offers a retransmission (RTX) stream for a codec the m-section does not carry.

#

  • Changed
    Subscribers of a simulcast publisher now climb to the best layer their link carries and back off on loss, instead of always receiving the cheapest layer. Nothing changes on the wire: still one stream per remote track, no layer-request frame. Calls.
  • Added
    A TypeScript client, @ollacore/client: typed REST for every operation and a room connection that handles tokens, reconnects, catch-up and dedupe. Pre-release — not yet published to npm, and calls are not in it yet. Client SDK.

#

  • Fixed
    before_seq without after_seq now pages backwards: the page is the limit messages just before it, still oldest first. It used to return the room's oldest messages whatever the cursor, so a client could not scroll up past its first page.
    What to do: To scroll up, pass the first message's event_seq as the next before_seq. Forward paging with after_seq is unchanged.
  • Added
    Every response carries an X-Request-Id header — yours, if you sent one. Our logs for that request carry the same id.
    What to do: Log it with failed calls and quote it when you write to support.
  • Security
    Erasing a room, a principal or a tenant now removes every copy of the erased events from the running service: the webhook delivery log and any fan-out or push work not yet done, as well as the messages. Scheduled retention does the same for the messages it expires. Backups are the exception: encrypted off-site backups keep erased data until they age out, within about six months; if one is restored, erasures made since are applied again, except that after the loss of the whole server we would ask you to repeat any erasure from its last day (Data lifecycle).
  • Changed
    The webhook delivery log keeps a delivery for 30 days after it settles (delivered, failed or cancelled), then deletes it; replaying an older delivery returns 404.
    What to do: Replay within 30 days, or keep your own copy of events you may need later.
  • Security
    A Web Push endpoint whose hostname resolves to a private, loopback or link-local address is no longer contacted; the subscription is dropped.
  • Changed
    Creating a directory app refuses a delivery_webhook_url that is not https:// or whose address is not public (private, loopback, link-local or otherwise reserved), with 400. A name that does not resolve yet is still accepted and checked at every delivery.
  • Changed
    Directory OTP challenges are deleted 24 hours after they expire.

#

  • Fixed
    The published OpenAPI document now matches the traffic it describes. The server did not change; the spec was wrong in these places:
    • Completing an upload returns status uploaded (or ready); the spec listed ready, pending, rejected.
    • Completing a multipart upload returns the same two fields as a single-part completion, not a full attachment.
    • A key created on the account plane has no tenant_id in its response; only the operator plane returns one.
    • Rooms carry e2ee; registered devices carry a nullable app_id; downloads include poster_url; attachment init accepts poster_of.
    • DELETE /v1/push/subscriptions returns {"removed": …}, not unsubscribed.
    • GET /v1/push/vapid-public-key needs no credential.
    • Minting a token declares the 400 (unknown role) and 403 (role above the member's).
    • PATCH /internal/v1/rooms/{room_id} declares its body (status, expires_at).
    What to do: Regenerate any client built from an earlier openapi.json. A hand-written client should accept uploaded as an upload status and must not require tenant_id on an account-plane key.

#

  • Security
    A minted session token can no longer carry more rights than the member has. The requested role must be owner, member or viewer (400 otherwise) and at or below the member's stored role (403 otherwise). Before, a higher role was granted.
    What to do: If you mint with an explicit role, check it never exceeds the member's.
  • Changed
    Both WebSockets re-check membership every 30 seconds. A socket of a principal removed from the room closes with 4403.
    What to do: Treat 4403 as final: do not reconnect.
  • Changed
    A Web Push endpoint must be https with a public host. Cleartext, and loopback, private or link-local IP addresses, are refused with 400.
  • Changed
    FCM credentials must use Google's own token_uri; anything else is refused with 400 when you store them.
  • Security
    Two concurrent verifies of the same OTP can no longer both succeed: exactly one wins.
  • Fixed
    One slow Web Push service no longer delays every other push: each delivery has a 10-second deadline.

#

  • Added
    POST /v1/directory/logout revokes the presented directory session, or every session of that user with {"all_devices": true}.
    What to do: Call it on sign-out and for "log out everywhere".
  • Changed
    Rate-limited responses (429) carry Retry-After, and the message names which limit was hit. otp/request has four: per app, per phone number, per IP and a resend cooldown.
    What to do: Back off by Retry-After rather than a fixed delay.
  • Changed
    POST /v1/directory/contacts/lookup is limited to 20 calls per session per minute.
  • Fixed
    Erasing a principal now also removes their directory identity: phone number, display name, sessions, devices and OTP rows. Erasing a tenant also removes its account.
  • Changed
    On Android, an incoming-call push is data-only, so your app can show a full-screen call. A message push still carries a notification.
    What to do: Implement onMessageReceived, or calls show nothing on Android.
  • Changed
    A ring expires after 45 seconds, so a device that was offline does not ring late, and a cancel replaces its ring.
  • Fixed
    A call ending no longer reaches Web Push as new_message. Web Push always carries type, room_id and, for calls, call_id.

#

  • Added
    Native push: the backend sends FCM and APNs notifications itself, with your own credentials, set once per directory app (PUT /v1/account/directory-apps/{app_id}/push).
  • Changed
    Pushes identify themselves, in FCM data and beside APNs aps: type and room_id always; call_id on the two call types; sender except on a message in an E2EE room. APNs pushes now play a sound, and E2EE rooms set mutable-content so a Notification Service Extension can decrypt.
    What to do: Route on type and room_id instead of parsing titles, and treat call_id and sender as optional.
  • Changed
    A ring is deduplicated by call_id, so a call is no longer swallowed by a message sent just before it. When the caller hangs up, a silent call.ended push goes to the members who were rung.
    What to do: Cancel the notification for that call_id when call.ended arrives.
  • Added
    Every operation has a recorded request and response in api-examples.json.

#

  • Added
    The turnkey directory: phone-OTP sign-up, profile, contact lookup, direct and group conversations, room tokens and push devices, under /v1/directory/*. Directory apps are created on the account plane and deliver codes through your webhook or your own Twilio, Plivo or MSG91 account.
  • Added
    GET /v1/directory/inbox: the chat list in one call, with names, last-message previews and unread counts.
  • Added
    call.started and call.ended events on the chat socket, to webhooks and as push, so an offline member can be rung.

#

  • Added
    Self-serve accounts: /v1/account/signup, login and logout, and API-key management (create, rotate, delete).
  • Changed
    A test key now works in its own data namespace and never sees live data. Anything a test key created before this is no longer visible to it. :: is reserved in tenant ids.
    What to do: Recreate test fixtures with your test key.
  • Added
    The /v1 stability policy: changes are additive, and a removal is announced 90 days ahead with Deprecation and Sunset headers.
  • Added
    Optional scheduled retention per plan: messages older than the plan's window are deleted, together with their attachments and stored files. Off unless set for your plan.

#

  • Added
    Erasure: DELETE /v1/server/rooms/{room_id} (scope rooms:delete) and POST /v1/server/principals/{principal_id}/erase (scope data:erase) delete rows and files.
  • Added
    Plans: per-key rate tiers, and optional monthly quotas on messages sent and attachment bytes. Over a quota the call fails with 402 quota_exceeded.
    What to do: Do not retry a 402 blindly: it lasts until the quota resets at the start of the month or the plan changes. Retry after either.
  • Changed
    Push, key-package and /v1/e2ee/* endpoints re-check that the caller is still a room member: a removed member gets 403 at once, not at token expiry. Push unsubscribe stays open to them.
  • Security
    Push subscriptions belong to the principal that created them; another principal can no longer overwrite or delete them.

#

  • Added
    Usage metering: GET /v1/server/usage (scope usage:read) sums message.sent and attachment.bytes over a window.

#

  • Added
    The customer server API, /v1/server/*, with tenant-pinned, scoped ak_ keys.
  • Added
    Webhooks: /v1/server/webhooks with signed delivery, retries, a delivery log and replay.