Skip to main content

Sync API

Sync is a general-purpose, real-time shared-state primitive for building presence indicators, collaborative widgets, live counters, or any app state multiple clients need to see change in real time — without standing up your own pub/sub infrastructure. It mirrors the Documents / Maps / Lists / Streams model of Twilio Sync: Documents, Maps, and Lists are durable (they persist until you delete them), Streams are ephemeral pub/sub with no persistence. Base path: /api/v1/sync WebSocket gateway: /api/v1/ws/sync — connect here to receive change events (document.updated, map.item.set, list.item.added, stream.message, etc.) as they happen. Authentication: API key (X-API-Key) or session JWT. Reads are open to any authenticated member; writes require an operator role (owner, admin, developer, or agent). The REST-write → WebSocket-read loop. Every mutation over REST publishes exactly one change event frame on the gateway for any client subscribed to the same object. Write over REST, render from the socket. Each example below names the frame it emits — subscribe with kind + name before you write, and the frame arrives within milliseconds. All responses use the standard envelope: the resource under data, plus a meta block with the request id and timestamp. Deletes return 204 No Content with an empty body.

Using the SDKs

The Node SDK exposes typed helpers for every Sync operation — client.sync.documents, client.sync.maps, client.sync.lists, client.sync.streams — and each sample group below leads with them. The Python SDK wraps the 8 core resources and reaches Sync through the generic client.request() escape hatch, which keeps auth, retries, and the { data, meta } envelope identical:
Go, Ruby, and PHP samples use plain standard-library HTTP. Full SDK index at SDK quickstart.

Documents

A Document is a single JSON object addressed by a unique name, with a monotonic revision that bumps on every update. Create a Document. The name is create-once: a second POST with an existing unique_name returns 409 CONFLICT. ttl (seconds) is optional; omit it and the Document persists until you delete it. The response is 201 Created with the Document at revision 1.
Subscribers on { "kind": "document", "name": "room_42_viewers" } receive the create as their first frame:
Replace a Document’s data. A POST to the Document’s own path replaces data wholesale and bumps the revision — revisions are monotonic per Document, so a client that missed a frame can compare revisions and re-hydrate from the REST GET below. A missing name returns 404 NOT_FOUND.
The socket frame carries the same bumped revision:
Read and delete. A GET re-hydrates the current data and revision (use it when a client reconnects and needs full state, not just deltas). DELETE removes the Document and answers 204 No Content; subscribers receive document.removed with no data payload.

Maps

A Map is an unordered, key-addressed collection of JSON values. Set an item. PUT is upsert: it creates the key when absent and replaces it when present, bumping the Map’s revision either way. Subscribers on the Map receive map.item.set naming the key.
List the items. The list endpoint returns every item in the Map as an array ordered by key — the shape a reconnecting client uses to rebuild its roster in one call.
Remove an item or the whole Map. A per-item DELETE answers 204 No Content and emits map.item.removed with the key but no data; deleting the Map itself emits map.removed. Presence clients drop the key on the first frame and reset their roster on the second.

Lists

A List is an ordered, append-indexed collection of JSON values. Append an item. POST appends and answers 201 Created with the item carrying its new 0-based index — the first append in a fresh List gets index 0. Subscribers receive list.item.added with the same index.
Replace the item at an index. PUT on an existing index swaps the value in place — indexes are stable and never shift on delete.
Remove. DELETE on an index answers 204 No Content and emits list.item.removed naming the index. Deleting the List itself answers 204 and emits list.removed.

Streams

A Stream is ephemeral pub/sub — nothing is stored. Only clients connected to the WebSocket gateway at publish time receive the message. Publish a message. The response is 201 Created with a sid you can correlate with the relayed frame, plus the data you sent and the publish ts. Because Streams hold no state, there is nothing to GET — a late subscriber simply starts from the next message.
Subscribers on { "kind": "stream", "name": "room_42_viewers" } receive the frame shown in the WebSocket consumer below:
Note the relayed data carries your payload plus the _sid echo — match on _sid to correlate a REST publish with its socket frame.

WebSocket consumer

The gateway exposes a small JSON protocol over the socket. Send subscribe / unsubscribe frames naming a kind (document, map, list, or stream) and a unique name; the server answers subscribed / unsubscribed to acknowledge, then relays change events.
Node.js
Auth from a browser. The browser WebSocket constructor cannot set headers, so encode the credential as a subprotocol token: new WebSocket(url, ["orbit-sync-v1", "Bearer.<jwt>"]) for a session JWT, or new WebSocket(url, ["ApiKey.<key>"]) for an API key. The server scans the Sec-WebSocket-Protocol list for the Bearer. / ApiKey. token and promotes it to the normal auth header shape during the upgrade handshake. Never embed a secret API key in front-end code shipped to end users — mint keys on your backend.
Node.js
Frame shape. Every relayed change event carries type (the discriminator), kind, unique_name, revision (absent for stream.message), key (map events), index (list events), data (absent on removal events), and a ts publish timestamp. To unsubscribe, send { "type": "unsubscribe", "kind": "...", "name": "..." }. Client ping frames get a pong answer; malformed frames get an error frame with a reason.

Ops debugging with wscat

Recipe — a collaborative presence indicator

A classic presence pattern: each client announces itself in a Map keyed by user id, and every client renders from those change events. REST writes, WebSocket reads.
Node SDK
Subscribe once to kind: "map", name: "room_42_presence", then each announce() call fans out to every connected viewer. To compute a live viewer counter instead of a per-user roster, have your backend publish to a Stream (POST /sync/streams/room_42_viewers/messages) and render stream.message events — no Map cleanup needed.

See also

  • Webhooks — for server-to-server event delivery instead of a client WebSocket
  • Video rooms — a common Sync use case is live participant/viewer counts alongside a video room
  • How request samples work — the six-language group each operation above carries