> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync API: real-time Documents, Maps, Lists, and Streams

> Real-time shared-state primitive — Documents, Maps, Lists, and ephemeral Streams — with change events relayed over a WebSocket gateway.

# 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`).

## Documents

A Document is a single JSON object addressed by a unique name, with a monotonic revision that bumps on every update.

| Method   | Path                            | Purpose                                                                       |
| -------- | ------------------------------- | ----------------------------------------------------------------------------- |
| `POST`   | `/api/v1/sync/documents`        | Create a Document (`unique_name`, optional `data`, optional `ttl` in seconds) |
| `GET`    | `/api/v1/sync/documents/{name}` | Get a Document's current `data` + revision                                    |
| `POST`   | `/api/v1/sync/documents/{name}` | Replace a Document's `data`, bumping the revision                             |
| `DELETE` | `/api/v1/sync/documents/{name}` | Delete a Document                                                             |

## Maps

A Map is an unordered, key-addressed collection of JSON values.

| Method   | Path                                   | Purpose                                    |
| -------- | -------------------------------------- | ------------------------------------------ |
| `GET`    | `/api/v1/sync/maps/{name}/items`       | List every key → value item                |
| `GET`    | `/api/v1/sync/maps/{name}/items/{key}` | Get one item                               |
| `PUT`    | `/api/v1/sync/maps/{name}/items/{key}` | Create or replace an item (optional `ttl`) |
| `DELETE` | `/api/v1/sync/maps/{name}/items/{key}` | Remove one item                            |
| `DELETE` | `/api/v1/sync/maps/{name}`             | Delete the whole Map                       |

## Lists

A List is an ordered, append-indexed collection of JSON values.

| Method   | Path                                      | Purpose                               |
| -------- | ----------------------------------------- | ------------------------------------- |
| `GET`    | `/api/v1/sync/lists/{name}/items`         | List every item, in order             |
| `POST`   | `/api/v1/sync/lists/{name}/items`         | Append an item, returns its new index |
| `PUT`    | `/api/v1/sync/lists/{name}/items/{index}` | Replace the item at an index          |
| `DELETE` | `/api/v1/sync/lists/{name}/items/{index}` | Remove the item at an index           |
| `DELETE` | `/api/v1/sync/lists/{name}`               | Delete the whole List                 |

## Streams

A Stream is ephemeral pub/sub — nothing is stored. Only clients connected to the WebSocket gateway at publish time receive the message.

| Method | Path                                   | Purpose                                              |
| ------ | -------------------------------------- | ---------------------------------------------------- |
| `POST` | `/api/v1/sync/streams/{name}/messages` | Publish a JSON message to every connected subscriber |

## 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.

```javascript theme={null}
// node: npm install ws — browser: use the built-in WebSocket
import WebSocket from "ws";

const API_KEY = "dv_live_sk_your_key_here"; // node only — see auth note below
const socket = new WebSocket(
  "wss://api.orbit.devotel.io/api/v1/ws/sync",
  { headers: { "X-API-Key": API_KEY } },
);

socket.addEventListener("open", () => {
  socket.send(
    JSON.stringify({
      type: "subscribe",
      kind: "document",
      name: "room_42_viewers",
    }),
  );
});

socket.addEventListener("message", (event) => {
  const msg = JSON.parse(event.data);
  switch (msg.type) {
    case "hello":
      console.log("connected", msg.tenantId);
      break;
    case "subscribed":
      console.log("watching", msg.kind, msg.name);
      break;
    case "document.updated":
      console.log(`revision ${msg.revision}:`, msg.data);
      break;
    case "map.item.set":
      console.log(`map key ${msg.key}:`, msg.data);
      break;
    case "list.item.added":
      console.log(`list index ${msg.index}:`, msg.data);
    case "stream.message":
      console.log("stream message:", msg.data);
      break;
    default:
      console.log("unhandled change event:", msg);
  }
});

socket.addEventListener("close", () => {
  // Reconnect with backoff — the server closes idle sockets and on shutdown.
});
```

**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.

```javascript theme={null}
const socket = new WebSocket("wss://api.orbit.devotel.io/api/v1/ws/sync", [
  "orbit-sync-v1",
  `ApiKey.${API_KEY}`, // or `Bearer.${sessionJwt}`
]);
```

**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

```bash theme={null}
wscat -c "wss://api.orbit.devotel.io/api/v1/ws/sync" \
  -H "X-API-Key: dv_live_sk_your_key_here"
> { "type": "subscribe", "kind": "map", "name": "room_42_presence" }
< { "type": "subscribed", "kind": "map", "name": "room_42_presence" }
< { "type": "map.item.set", "kind": "map", "unique_name": "room_42_presence", "key": "user_123", "revision": 7, "data": { "status": "online" }, "ts": 1756147200000 }
```

## 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.

```javascript theme={null}
// 1. Announce yourself (API key minted by your backend — keep it server-side).
async function announce(userId, status) {
  await fetch(
    `https://api.orbit.devotel.io/api/v1/sync/maps/room_42_presence/items/${userId}`,
    {
      method: "PUT",
      headers: {
        "X-API-Key": "dv_live_sk_your_key_here",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        data: { status, lastSeenAt: Date.now() }, // ttl (seconds) optional
      }),
    },
  );
}

// 2. Every client listens for join/leave/refresh events.
const presence = new Map();
socket.addEventListener("message", (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === "map.item.set") {
    presence.set(msg.key, msg.data);
  }
  if (msg.type === "map.item.removed" || msg.type === "map.removed") {
    presence.delete(msg.key ?? undefined);
    if (msg.type === "map.removed") presence.clear();
  }
  renderPresence(presence); // your UI update
});
```

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](/webhooks/overview) — for server-to-server event delivery instead of a client WebSocket
* [Video rooms](/guides/video-room-access-tokens) — a common Sync use case is live participant/viewer counts alongside a video room
