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:What to do: Nothing for unrestricted members. A restricted member's client must accept re-offers that remove or add sections.
subscribe(all,none, or anallowlistof up to 64 publishers) is enforced inside the SFU on every packet, androster(all,authorized-only,none) filterspeers/peer.joined/peer.leftper recipient. A change stops or starts forwarding, re-offers the connection and re-sendspeerswithin about a second. API reference. - Added Device limits:What to do: Mint each device's tokens with a stable
"devices": {"max_active": 1..16, "on_limit": "allow" | "reject-new" | "replace-old"}on a member's policy. A device is the session token'sdevice_id. At the limitreject-newsends anerroreventdevice_limitand closes the newcomer 4411;replace-oldcloses the oldest device 4412policy_changed; the samedevice_idreconnecting replaces its predecessor (4412device_reconnected). A lowered limit applies to live devices. Members without adevicessection behave as before.device_id, and treat 4411 and 4412 as "stop, do not reconnect". - Added Webhook eventsWhat to do: An endpoint receives these events only if its
rtc.device_conflictandrtc.policy.deniedreport 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/forwardingreads who receives whose media right now, with the simulcast layer.event_typeslist them; endpoints cannot be edited, so create one that does.
#
- Added Per-member media policy: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
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 answeredinactivewith anerrorevent beside the answer, and a publication the policy withdraws must be re-offered without it within 5 s or the connection is closed 4409policy_apply_failed.chatandattachmentssections refuse HTTP calls withchat_send_forbidden,chat_history_forbidden,attachment_upload_forbiddenorattachment_download_forbidden, and withdrawinghistorycloses the chat socket 4410policy_changed. Revisions are yours and monotonic;DELETEtakes its own?revision=and leaves a tombstone. API reference.errorevent. - Added A room can declare which end-to-end encryption scheme its members run:What to do: Nothing. Declare it on new encrypted rooms so your clients can rely on it.
"e2ee_scheme": "mls"or"megolm"on room creation (withe2ee: 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. - Added MLS group state lives on the server: after each commit the committerWhat to do: MLS clients: publish after every commit and build Welcomes without the
PUTs/v1/rooms/{room_id}/mls/group-infowith the GroupInfo and, optionally, the ratchet tree (base64, opaque; 64 KiB / 256 KiB); a joinerGETs 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.ratchet_treeextension.
#
- Added MLS key packages follow the RFC 9750 directory rules. A device may publish one last-resort package (What to do: Keep a last-resort package published per device and replace it once
"last_resort": true, exactly one package per request; a new upload replaces it): when the device's regular pool is dry it is served flaggedlast_resort: trueand not consumed, so a join never fails for want of a package. A package may carryexpires_in_seconds(up to a year); expired packages are never served.GET /v1/keypackages/countnow reportsremainingregular packages and whether alast_resortone is on file. End-to-end encryption.remainingreaches 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-madematrix-sdk-cryptopackages. 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-sectionWhat 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
inactiveand resumed later on the same mid (sendonlyagain, same RIDs, areplaceTrack) 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 answeredrecvonlyfor aninactiveoffer, which browsers refuse. Audio and single-encoding video were never affected. Calls.ontrackon each resume.
#
- Added An ephemeralWhat 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).
broadcastframe on the chat socket:{"type":"broadcast","payload":{"body":{…},"to"?:"<principal_id>"}}fansbodyout, unchanged, to the members connected at that moment — or to one member's sockets withto— as abroadcastevent withevent_id: 0and payload{from, body, to?}. Nothing is stored,catchupnever replays it, and your own sockets receive it too.bodyis 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_limitedwith 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. - Added Per-subscriber simulcast layer preference, set by your server:What to do: Needs
PUT /v1/server/rooms/{room_id}/members/{principal_id}/preferences(scopemembers:write) with{"tracks":[{"publisher":"<principal_id>","track_id":"<publisher's track id>","layer":"low"|"medium"|"high"|"auto"}]}, andGET(scoperooms:read) to read it back within_call. Keyed by what the member's client already sees — the msid achatbox.msid-principalsocket 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 eachPUT,[]clears. Details: Calls.members:writeon the key forPUTandrooms:readforGET. 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 withWhat to do: Reconnect on
1001 server shutting downwithin milliseconds of the signal, and the service answers again within a few seconds; a request in the gap gets502from the edge. There is no reconnect rate limit on either socket. Written down in Deploys.1001with exponential backoff and jitter from about one second, thencatchup. 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. AWhat to do: Render "Forwarded" when
message.send(and both REST sends) may carry"mentions": [<principal_id>, …]insidebody— 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 carriesforward_depth, and the source is never named. Readable kinds only; not a deleted message; not into an E2EE room (409). Chat.forward_depthis present, and@namesfrommentions. - Added Directory groups gain a
descriptionand anicon_attachment_id(PATCH /v1/directory/conversations/{room_id}, owner only; each change is posted into the room as asystemmessage), any number of owners (PUT …/members/{user_id}with{"role": "owner" | "member"}, announced asmember.updated), and invite links: owners create, list and revoke them under…/{room_id}/invites; anyone in the tenant previews and joins withPOST /v1/directory/invites/previewand/join, token in the body. Turnkey identity.
#
- Added A directory profile: anWhat to do: Cache photo bytes by
aboutline (up to 256 characters, viaPATCH /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 — carriesaboutandavatar_version, never a URL;GET /v1/directory/users/{user_id}/avatar?size=thumb|fullis a 302 to the bytes withETag=avatar_version. Turnkey identity.avatar_versionand sendIf-None-Matchto get a 304 instead of a redirect. - Added RTC subprotocolWhat to do: Optional. Check
chatbox.msid-principal: list it ahead ofchatboxwhen 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>, soevent.streams[0].idis the verified principal in every browser. Optional and per socket; a socket that offers onlychatboxkeeps thepeer-<client_id>-…form. Calls.ws.protocolonce 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 withWhat to do: Handle
409and nothing changes; restoring a removed member or changing a role never needs a seat. Directory groups are created with the same cap.409on member adds. - Added Directory group management for your users:
GET /v1/directory/conversations/{room_id}and…/membersfor any member; owners add up to 256 members per call (POST …/members), remove one (DELETE …/members/{user_id}); anyone leaves withPOST …/leave— a lone owner's seat passes to the longest-standing member, and an empty group closes. Changes arrive asmember.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}/mutewith{"duration": "8h" | "1w" | "always"},DELETEto unmute; the inbox showsmuted_until. Your server does the same for a member withPUT/DELETE /v1/server/rooms/{room_id}/members/{principal_id}/mute(scopemembers:write). Push only: the socket still delivers and a call still rings. Private — no event is emitted. - Added Block.What to do: Handle the
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 answerblocked(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, reasonblocked); shared groups are unaffected; the blocked person is not told. Turnkey identity.blockederror 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 answers200.
#
- 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 atWhat to do: Stop calling
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/v1policy: it answers an empty list, every response carriesDeprecationandSunsetheaders, and it may be removed on or after 2027-01-05. What remains.GET /v1/rooms/{room_id}/recordings; nothing it could return exists.
#
- Added The RTC socket sends roster frames, discriminated byWhat to do:
event:peersonce on connect, before any SDP, listing everyone already in the call except you;peer.joined(an upsert) andpeer.left, each withclient_idandprincipal_id. Calls.client_idis a decimal string that exceeds 2^53 — compare it as a string, never as a number.peer.leftis a roster hint, not a media event: do not tear down tracks on it. Ignoreeventvalues 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 What to do: To scroll up, pass the first message's
before_seqwithoutafter_seqnow pages backwards: the page is thelimitmessages 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.event_seqas the nextbefore_seq. Forward paging withafter_seqis unchanged. - Added Every response carries anWhat to do: Log it with failed calls and quote it when you write to support.
X-Request-Idheader — yours, if you sent one. Our logs for that request carry the same id. - 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 returnsWhat to do: Replay within 30 days, or keep your own copy of events you may need later.
404. - 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_urlthat is nothttps://or whose address is not public (private, loopback, link-local or otherwise reserved), with400. 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:What to do: Regenerate any client built from an earlier
- Completing an upload returns status
uploaded(orready); the spec listedready,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_idin its response; only the operator plane returns one. - Rooms carry
e2ee; registered devices carry a nullableapp_id; downloads includeposter_url; attachment init acceptsposter_of. DELETE /v1/push/subscriptionsreturns{"removed": …}, notunsubscribed.GET /v1/push/vapid-public-keyneeds 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).
openapi.json. A hand-written client should acceptuploadedas an upload status and must not requiretenant_idon an account-plane key. - Completing an upload returns status
#
- Security A minted session token can no longer carry more rights than the member has. The requestedWhat to do: If you mint with an explicit role, check it never exceeds the member's.
rolemust beowner,memberorviewer(400 otherwise) and at or below the member's stored role (403 otherwise). Before, a higher role was granted. - Changed Both WebSockets re-check membership every 30 seconds. A socket of a principal removed from the room closes withWhat to do: Treat
4403.4403as final: do not reconnect. - Changed A Web Push endpoint must be
httpswith 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 What to do: Call it on sign-out and for "log out everywhere".
POST /v1/directory/logoutrevokes the presented directory session, or every session of that user with{"all_devices": true}. - Changed Rate-limited responses (429) carryWhat to do: Back off by
Retry-After, and the message names which limit was hit.otp/requesthas four: per app, per phone number, per IP and a resend cooldown.Retry-Afterrather than a fixed delay. - Changed
POST /v1/directory/contacts/lookupis 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 carriestype,room_idand, 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 FCMWhat to do: Route on
dataand beside APNsaps:typeandroom_idalways;call_idon the two call types;senderexcept on a message in an E2EE room. APNs pushes now play a sound, and E2EE rooms setmutable-contentso a Notification Service Extension can decrypt.typeandroom_idinstead of parsing titles, and treatcall_idandsenderas optional. - Changed A ring is deduplicated byWhat to do: Cancel the notification for that
call_id, so a call is no longer swallowed by a message sent just before it. When the caller hangs up, a silentcall.endedpush goes to the members who were rung.call_idwhencall.endedarrives. - 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.startedandcall.endedevents on the chat socket, to webhooks and as push, so an offline member can be rung.
#
- Added Self-serve accounts:
/v1/account/signup,loginandlogout, and API-key management (create, rotate, delete). - Changed AWhat to do: Recreate test fixtures with your test key.
testkey 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. - Added The
/v1stability policy: changes are additive, and a removal is announced 90 days ahead withDeprecationandSunsetheaders. - 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}(scoperooms:delete) andPOST /v1/server/principals/{principal_id}/erase(scopedata: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 402What 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.
quota_exceeded. - 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(scopeusage:read) sumsmessage.sentandattachment.bytesover a window.
#
- Added The customer server API,
/v1/server/*, with tenant-pinned, scopedak_keys. - Added Webhooks:
/v1/server/webhookswith signed delivery, retries, a delivery log and replay.