---
name: deewan-api
description: Integrate Deewan video meetings into an application through the Deewan REST API, the @deewan/sdk server SDK or the @deewan/client browser SDK — creating rooms, sending users into meetings with join links, an embedded meeting or media tokens, scheduling meetings, moderating participants, running polls, hosting webinars with a stage and an audience, fetching recordings, and receiving signed webhooks. Use when a task mentions Deewan, deewan.io, embedding video meetings, or the dwn_ API keys.
---

# Deewan API

Deewan hosts end-to-end encrypted video meetings. Your backend controls them over HTTPS.

- Base URL: `https://deewan.io/api/v1`
- Auth: `Authorization: Bearer <DEEWAN_API_KEY>` (keys start with `dwn_live_` or `dwn_test_`)
- Format: JSON in and out; timestamps ISO 8601 UTC; ids are UUIDs
- Docs: https://deewan.io/en/docs · OpenAPI: https://deewan.io/api/v1/openapi.json
- Server SDK: `npm install @deewan/sdk` (Node 18+, Bun, Deno, edge)
- Browser SDK (custom UI only): `npm install @deewan/client`

## Rules you must follow

1. **Never expose the API key.** Read it from an environment variable on the server (`process.env.DEEWAN_API_KEY`). Never put it in client-side code, a mobile app, a public repo, logs, or example output. If the user's code does this, fix it.
2. **Authorize before issuing access.** Create join links and tokens only after the app has checked that the current user may join. Issue one link or token per person; never share one between users.
3. **Tokens are per-user secrets.** The `POST /rooms/{id}/tokens` response includes `e2ee_passphrase`; send it only to that user's client over HTTPS.
4. **Verify every webhook.** Check `Deewan-Signature` (HMAC-SHA256) against the *raw* body and reject timestamps older than 5 minutes. Use `verifyWebhook` from the SDK. Respond 2xx within 10 s; deduplicate by event `id`.
5. **Use the narrowest scopes.** A service that only sends people into existing rooms needs `rooms:read` + `tokens:create`.
6. **A recording's download key decrypts the meeting.** Fetch `POST /recordings/{id}/download` from the server only, never from a browser, and keep it out of logs.
7. **Handle errors and limits.** Errors look like `{ "error": { "code", "message", "details"? }, "request_id" }`. Retry `429` (honour `Retry-After`) and `5xx` with exponential backoff; the SDK does this. Limit: 120 requests/minute per key.

## Choosing how people join

| Need | Use |
| --- | --- |
| Meetings in an afternoon, no UI work | `POST /rooms/{id}/join-links` → redirect the user to `url` (Deewan's meeting interface, no account needed, skips the waiting room, expires) |
| Deewan's interface *inside* the product | the same join link in an `<iframe allow="camera; microphone; display-capture; autoplay; picture-in-picture">` |
| Fully custom in-app UI | `POST /rooms/{id}/tokens` → connect with `@deewan/client` using `server_url`, `token` and `e2ee_passphrase` |

Default to join links or the embedded frame: they give the full Deewan experience (lobby, stage, chat, reactions, captions, polls, recording, picture-in-picture). Only reach for tokens when the user explicitly wants to build their own interface.

## Recipes

### Create a room and a join link (SDK)

```ts
import { Deewan } from "@deewan/sdk";

const deewan = new Deewan({ apiKey: process.env.DEEWAN_API_KEY! });

const room = await deewan.rooms.create({
  name: "Onboarding call",
  settings: { admission: "knock", screen_share: "hosts" },
});

const link = await deewan.rooms.createJoinLink(room.id, {
  name: currentUser.displayName, // shown to others
  role: currentUser.isOrganizer ? "host" : "participant",
  user_id: currentUser.id,       // your id; stable identity across links
  expires_in: 3600,
  locale: "en",                  // or "ar"
});
// redirect the user to link.url
```

### Same with fetch

```ts
const res = await fetch("https://deewan.io/api/v1/rooms", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.DEEWAN_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Onboarding call" }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const room = await res.json();
```

### Embed Deewan's meeting in your own page

```tsx
// server: const link = await deewan.rooms.createJoinLink(roomId, { name, user_id });
<iframe
  src={link.url}
  allow="camera; microphone; display-capture; autoplay; clipboard-write; picture-in-picture"
  style={{ width: "100%", height: "100%", border: 0 }}
/>
```

### Custom client with end-to-end encryption (browser)

```ts
// server: const t = await deewan.rooms.createToken(roomId, { name, user_id });
import { joinMeeting } from "@deewan/client";

const meeting = await joinMeeting(
  { serverUrl: t.server_url, token: t.token, e2eePassphrase: t.e2ee_passphrase },
  { publish: { camera: true, microphone: true } },
);
meeting.on("participants", render);
meeting.on("trackSubscribed", ({ attach, kind }) => {
  const el = document.createElement(kind === "video" ? "video" : "audio");
  el.autoplay = true;
  attach(el);
});
```

### Polls and recordings

```ts
const poll = await deewan.polls.create(roomId, {
  question: "Ready to move on?",
  options: ["Yes", "Not yet"],
  results: "after_close",   // "live" | "after_vote" | "after_close"
});
await deewan.polls.close(poll.id);
const { data: polls } = await deewan.polls.list(roomId); // counts per option

// Breakout rooms: split the meeting, move people, bring everyone back
await deewan.breakouts.start(roomId, { rooms: 3, assign: "auto", duration_seconds: 600 });
await deewan.breakouts.end(roomId, { warn: true });

// Recordings are encrypted in the host's browser; the SDK decrypts locally.
const { data: recordings } = await deewan.recordings.list({ room_id: roomId });
const { bytes, mimeType } = await deewan.recordings.download(recordings[0].id);
```

### Webinars

A webinar is a room with a stage and an audience: hosts publish camera, microphone and screen; everyone else watches, chats and raises a hand until invited to speak. The media server enforces this — an audience pass carries no right to publish — so it holds even against a modified client. Up to 500 people (a regular meeting holds 300).

```ts
const room = await deewan.rooms.create({ name: "Product launch", settings: { type: "webinar", access: "link" } });
const host = await deewan.rooms.createJoinLink(room.id, { name: "Sara Ahmed", role: "host" });
const seat = await deewan.rooms.createJoinLink(room.id, { name: "Omar Saleh" }); // joins as audience

await deewan.rooms.inviteToSpeak(room.id, identity);   // they may turn on mic and camera
await deewan.rooms.moveToAudience(room.id, identity);  // back to watching
```

### Webhook receiver (Next.js route)

```ts
import { verifyWebhook } from "@deewan/sdk";

export async function POST(request: Request) {
  const raw = await request.text();
  try {
    const event = await verifyWebhook(raw, request.headers.get("deewan-signature"), process.env.DEEWAN_WEBHOOK_SECRET!);
    switch (event.type) {
      case "room.started": /* … */ break;
      case "participant.joined": /* … */ break;
      case "room.finished": /* … */ break;
    }
    return new Response(null, { status: 204 });
  } catch {
    return new Response("invalid signature", { status: 400 });
  }
}
```

Events: `room.created`, `room.updated`, `room.deleted`, `room.started`, `room.finished`, `participant.joined`, `participant.left`, `participant.stage_changed`, `meeting.scheduled`, `meeting.updated`, `meeting.cancelled`, `poll.created`, `poll.closed`, `breakout.started`, `breakout.ended`, `recording.ready` (and `webhook.test`). Payload: `{ id, type, created_at, data }`.

### Moderation

```ts
const { data: people } = await deewan.rooms.listParticipants(roomId);
await deewan.rooms.muteParticipant(roomId, people[0].identity);
await deewan.rooms.removeParticipant(roomId, people[0].identity);
await deewan.rooms.update(roomId, { locked: true }); // no one new can join
await deewan.rooms.end(roomId);                      // disconnect everyone
await deewan.rooms.makeCohost(roomId, identity);     // host powers for this meeting (accounts only)
const { data: waiting } = await deewan.rooms.listWaiting(roomId);
await deewan.rooms.admit(roomId, waiting[0].identity); // or deny
```

### Meetings and breakout rooms

```ts
// Repeats weekly at 10:00 Amman time; also lands in the key owner's Google/Outlook calendar.
const m = await deewan.meetings.create({ title: "Weekly sync", starts_at, ends_at, recurrence: "weekly", timezone: "Asia/Amman" });
await deewan.meetings.update(m.id, { title: "Weekly sync (moved)" });

await deewan.breakouts.move(roomId, participant, breakoutId, { canReturn: false });
await deewan.breakouts.addRoom(roomId, "Overflow");
await deewan.breakouts.renameRoom(roomId, breakoutId, "Design review");
await deewan.breakouts.removeRoom(roomId, breakoutId); // its people go back to the main room
```

## Room settings

`access`: `"link"` (anyone with the link can join; with `admission: "knock"` everyone but hosts waits) or `"members"` · `admission`: `"direct"` | `"knock"` · `mute_on_entry`, `camera_off_on_entry`: boolean · `screen_share`: `"everyone"` | `"hosts"` · `reactions`, `chat`: boolean · `encryption`: `"standard"` | `"strict"` (strict: key only on participants' devices; tokens return `409`, use join links; can't change mid-meeting) · `recordings`: `"workspace"` | `"end_to_end"` · `before_host`: `"join"` | `"wait"` · `type`: `"meeting"` | `"webinar"` (webinar: only hosts publish until invited with `inviteToSpeak`; up to 500 people). Changes apply immediately to a meeting in progress. Join links and tokens bypass the waiting room; a **locked** room refuses new links/tokens (`409`) and non-host joins.

## Scopes

`rooms:read`, `rooms:write`, `meetings:read`, `meetings:write`, `tokens:create`, `participants:read`, `participants:write`, `polls:read`, `polls:write`, `breakouts:read`, `breakouts:write`, `recordings:read`, `recordings:write`, `webhooks:manage`. Keys belong to one workspace (or a personal account) and only see its resources. A key stops working if its creator loses admin rights.

## Endpoint reference

### Account

- `GET /me`. Inspect the key. Returns the key's id, workspace, scopes and environment. Useful as a connectivity check.
  Returns: An `api_key` object.

### Rooms

- `GET /rooms` — scope `rooms:read`. List rooms. Rooms in the key's scope, newest first. Paginate with `starting_after` set to the last id of the previous page.
  Query: `limit`: integer; `starting_after`: uuid
  Returns: A list of `room` objects.
- `POST /rooms` — scope `rooms:write`. Create a room. Creates a room with a permanent link. Rooms created through the API default to `access: "link"`.
  Body: `name` (required): string; `description`: string | null; `settings.access`: "members" | "link"; `settings.admission`: "direct" | "knock"; `settings.mute_on_entry`: boolean; `settings.camera_off_on_entry`: boolean; `settings.screen_share`: "everyone" | "hosts"; `settings.reactions`: boolean; `settings.chat`: boolean; `settings.encryption`: "standard" | "strict"; `settings.recordings`: "workspace" | "end_to_end"; `settings.before_host`: "join" | "wait"
  Returns: The new `room`.
- `GET /rooms/{room_id}` — scope `rooms:read`. Retrieve a room. Returns one room.
  Returns: A `room`.
- `PATCH /rooms/{room_id}` — scope `rooms:write`. Update a room. Changes the name, description, lock or settings. Setting changes apply immediately to a meeting in progress (who may share, chat and react).
  Body: `name`: string; `description`: string | null; `locked`: boolean; `settings.access`: "members" | "link"; `settings.admission`: "direct" | "knock"; `settings.mute_on_entry`: boolean; `settings.camera_off_on_entry`: boolean; `settings.screen_share`: "everyone" | "hosts"; `settings.reactions`: boolean; `settings.chat`: boolean; `settings.encryption`: "standard" | "strict"; `settings.recordings`: "workspace" | "end_to_end"; `settings.before_host`: "join" | "wait"
  Returns: The updated `room`.
- `DELETE /rooms/{room_id}` — scope `rooms:write`. Delete a room. Deletes the room and ends any meeting in it. Its link stops working.
  Returns: The deleted `room` with `deleted: true`.

### Joining

- `POST /rooms/{room_id}/join-links` — scope `tokens:create`. Create a join link. A personal, expiring link into Deewan's hosted meeting page. The person needs no Deewan account and does not wait in the waiting room — your server already decided they may join. Send each person their own link.
  Body: `name` (required): string; `role`: "host" | "participant"; `user_id`: string; `expires_in`: integer; `locale`: "en" | "ar"; `redirect_url`: string; `branding`: "deewan" | "none"
  Returns: A `join_link`.
- `POST /rooms/{room_id}/tokens` — scope `tokens:create`. Create a media token. Credentials for an interface you build yourself, using `@deewan/client` in the browser. Valid for 10 minutes to connect. Includes the meeting's end-to-end encryption passphrase: treat the whole response as a secret for that one person. For Deewan's own interface, create a join link instead. Rooms with `strict` encryption return `409`: their key never exists on our servers, so there is no passphrase to give.
  Body: `name` (required): string; `role`: "host" | "participant"; `user_id`: string; `avatar`: string
  Returns: A `room_token`.

### Participants

- `GET /rooms/{room_id}/participants` — scope `participants:read`. List participants. Who is in the meeting right now. Empty when the meeting isn't running.
  Returns: A list of `participant` objects.
- `POST /rooms/{room_id}/participants/{identity}/mute` — scope `participants:write`. Mute a participant. Mutes someone's microphone. Only they can unmute themselves.
  Returns: `{ object, identity, audio_muted: true }`
- `POST /rooms/{room_id}/participants/{identity}/cohost` — scope `participants:write`. Make a co-host. Gives someone in the meeting host powers for this meeting only: admitting, muting and removing people, and running breakout rooms and polls. Not the room itself — a co-host can't change its saved settings or delete it — and it ends when the meeting ends. Works for Deewan accounts and for your own users who joined with a token from this API — for them it lasts while they're connected (to make someone a host from the start, create their token with `role: "host"`). The key's creator must manage the room.
  Returns: `{ object, identity, cohost: true }`
- `DELETE /rooms/{room_id}/participants/{identity}/cohost` — scope `participants:write`. Remove a co-host. Takes co-host powers back. Never demotes one of the room's own hosts (`400 not_a_cohost`).
  Returns: `{ object, identity, cohost: false }`
- `POST /rooms/{room_id}/participants/{identity}/stage` — scope `participants:write`. Invite to speak (webinars). In a webinar, puts someone from the audience on stage: the media server now lets them publish their microphone and camera (and their screen, if the room allows it). Lasts while they're connected. Hosts are always on stage (`400 already_host`); a meeting has no audience (`400 not_a_webinar`). The key's creator must manage the room.
  Returns: `{ object, identity, on_stage: true }`
- `DELETE /rooms/{room_id}/participants/{identity}/stage` — scope `participants:write`. Move to audience (webinars). Sends someone on stage back to the audience. The media server stops their microphone, camera and screen at once.
  Returns: `{ object, identity, on_stage: false }`
- `GET /rooms/{room_id}/waiting` — scope `participants:read`. List the waiting room. People asking to join, oldest first. People wait in rooms set to "Ask to join", and outsiders wait in members-only rooms. Nobody is kept waiting more than an hour.
  Returns: A list of `waiting_participant` objects.
- `POST /rooms/{room_id}/waiting/{identity}` — scope `participants:write`. Admit or deny. Lets someone in from the waiting room, or turns them away.
  Body: `decision` (required): "admit" | "deny"
  Returns: `{ object: "waiting_participant", identity, decision }`
- `DELETE /rooms/{room_id}/participants/{identity}` — scope `participants:write`. Remove a participant. Removes someone from the meeting. Lock the room as well if they shouldn't come back.
  Returns: `{ object, identity, removed: true }`
- `POST /rooms/{room_id}/end` — scope `participants:write`. End the meeting. Disconnects everyone. The room and its link remain.
  Returns: `{ object: "room", id, ended: true }`

### Meetings

- `GET /meetings` — scope `meetings:read`. List scheduled meetings. Meetings overlapping a time window, soonest first.
  Query: `from`: ISO 8601; `to`: ISO 8601; `limit`: integer
  Returns: A list of `meeting` objects.
- `POST /meetings` — scope `meetings:write`. Schedule a meeting. Adds a meeting to the workspace calendar, once or on a repeating rule, optionally in a room. It is also written to the key creator's connected Google and Outlook calendars, and sends `meeting.scheduled`.
  Body: `title` (required): string; `starts_at` (required): ISO 8601; `ends_at` (required): ISO 8601; `room_id`: uuid; `description`: string | null; `recurrence`: "none" | "daily" | "weekdays" | "weekly" | "biweekly" | "monthly"; `recurrence_until`: ISO 8601 | null; `timezone`: string | null
  Returns: The new `meeting`.
- `GET /meetings/{meeting_id}` — scope `meetings:read`. Retrieve a meeting. Returns one scheduled meeting.
  Returns: A `meeting`.
- `PATCH /meetings/{meeting_id}` — scope `meetings:write`. Update a meeting. Changes a meeting. Fields you leave out keep their value. For a repeating meeting every occurrence changes. Connected calendars are updated too, and `meeting.updated` is sent.
  Body: `title`: string; `starts_at`: ISO 8601; `ends_at`: ISO 8601; `description`: string | null; `recurrence`: "none" | "daily" | "weekdays" | "weekly" | "biweekly" | "monthly"; `recurrence_until`: ISO 8601 | null; `timezone`: string | null
  Returns: The updated `meeting`.
- `DELETE /meetings/{meeting_id}` — scope `meetings:write`. Cancel a meeting. Removes the meeting from the calendar, and from connected Google and Outlook calendars. The room is not affected.
  Returns: The cancelled `meeting` with `deleted: true`.

### Polls

- `GET /rooms/{room_id}/polls` — scope `polls:read`. List polls. Polls asked in this room in the last 24 hours, newest first, with their counts. Voter names are never returned.
  Returns: A list of `poll` objects.
- `POST /rooms/{room_id}/polls` — scope `polls:write`. Create a poll. Asks everyone in the meeting a question. It appears immediately for people in Deewan's meeting interface.
  Body: `question` (required): string; `options` (required): string[]; `kind`: "single" | "multiple"; `anonymous`: boolean; `results`: "live" | "after_vote" | "after_close"
  Returns: The created `poll`.
- `PATCH /polls/{poll_id}` — scope `polls:write`. Close or reopen a poll. Closing stops voting and reveals results that were hidden until the end.
  Body: `status` (required): "open" | "closed"
  Returns: The updated `poll`.
- `DELETE /polls/{poll_id}` — scope `polls:write`. Delete a poll. Removes the poll and its votes for everyone.
  Returns: `{ object: "poll", id, deleted: true }`.

### Breakout rooms

- `GET /rooms/{room_id}/breakouts` — scope `breakouts:read`. Get breakout rooms. The breakout session running in this meeting, with who is placed in each room and who is still unassigned, plus the ten most recent sessions. `active` is `null` when nothing is running.
  Returns: `{ object: "breakouts", room_id, active, history }`, where `active` is a `breakout_session`.
- `POST /rooms/{room_id}/breakouts` — scope `breakouts:write`. Start breakout rooms. Splits the people in the meeting into smaller private rooms. Each breakout has its own end-to-end encryption key, so nobody can join or decrypt a room they were not placed in. Only one session can be open at a time; a second start returns `409`. Hosts stay in the main room unless `include_hosts` is set, and can visit any breakout.
  Body: `rooms` (required): integer | string[]; `assign`: "auto" | "manual" | "none"; `placements`: { participant, room }[]; `auto_join`: boolean; `allow_return`: boolean; `self_select`: boolean; `duration_seconds`: integer | null; `include_hosts`: boolean
  Returns: The started `breakout_session`.
- `POST /rooms/{room_id}/breakouts/assignments` — scope `breakouts:write`. Move a participant. Places one person in a breakout room, moves them to another, or sends them back to the main room. It takes effect straight away for people who are connected, and holds for people who reconnect.
  Body: `participant` (required): string; `breakout_id` (required): uuid | null; `can_return`: boolean
  Returns: The updated `breakout_session`.
- `POST /rooms/{room_id}/breakouts/rooms` — scope `breakouts:write`. Add a room. Adds a room to the running session, with its own end-to-end key. Up to 50 rooms.
  Body: `name`: string
  Returns: The updated `breakout_session`.
- `PATCH /rooms/{room_id}/breakouts/rooms/{breakout_id}` — scope `breakouts:write`. Rename a room. Renames a room. People inside see the new name at once.
  Body: `name` (required): string
  Returns: The updated `breakout_session`.
- `DELETE /rooms/{room_id}/breakouts/rooms/{breakout_id}` — scope `breakouts:write`. Remove a room. Removes a room from the running session. Anyone in it goes back to the main room. A session keeps at least one room.
  Returns: The updated `breakout_session`.
- `POST /rooms/{room_id}/breakouts/announce` — scope `breakouts:write`. Broadcast a message. Shows a short message to everyone in the meeting and in every breakout room at once, for example "Five minutes left". Announcements are shown as a banner and are not end-to-end encrypted, so do not put anything private in them. Limited to ten a minute.
  Body: `message` (required): string
  Returns: `{ object: "breakout_announcement", room_id, sent: true }`.
- `POST /rooms/{room_id}/breakouts/end` — scope `breakouts:write`. End breakout rooms. Brings everyone back to the main room. With `warn: true` people see a 60-second countdown and the rooms close when it ends; otherwise they close at once. Ending the whole meeting also ends any open breakouts.
  Body: `warn`: boolean
  Returns: `{ object: "breakouts", room_id, ended, closing, ends_at }`.

### Recordings

- `GET /recordings` — scope `recordings:read`. List recordings. Recordings made in this workspace's rooms, newest first.
  Query: `room_id`: uuid; `limit`: integer
  Returns: A list of `recording` objects.
- `GET /recordings/{recording_id}` — scope `recordings:read`. Retrieve a recording. Metadata for one recording, including how many encrypted parts it has.
  Returns: A `recording` object.
- `POST /recordings/{recording_id}/download` — scope `recordings:read`. Get the parts and key. Everything needed to rebuild the file: the part URLs and the key for this one recording. Each part is a 12-byte IV followed by AES-256-GCM ciphertext, authenticated with `deewan-recording:<recording id>:<index>`. The SDK's `recordings.download()` does this for you. Treat the key like a password.
  Returns: A `recording.download` object.
- `GET /recordings/{recording_id}/parts/{index}` — scope `recordings:read`. Download one part. One encrypted part, exactly as stored. The response is binary, not JSON.
  Returns: `application/octet-stream` — IV + ciphertext.
- `DELETE /recordings/{recording_id}` — scope `recordings:write`. Delete a recording. Deletes the recording and its stored parts. This cannot be undone.
  Returns: `{ object: "recording", id, deleted: true }`.

### Webhooks

- `GET /webhooks` — scope `webhooks:manage`. List endpoints. Webhook endpoints in the key's scope.
  Returns: A list of `webhook_endpoint` objects.
- `POST /webhooks` — scope `webhooks:manage`. Create an endpoint. Registers a public HTTPS endpoint. The response includes `secret`, shown only once — store it to verify signatures. Up to 10 endpoints per scope.
  Body: `url` (required): string; `events` (required): string[]; `description`: string | null; `enabled`: boolean
  Returns: The new `webhook_endpoint` plus `secret`.
- `PATCH /webhooks/{webhook_id}` — scope `webhooks:manage`. Update an endpoint. Change the URL, events, description, or pause it with `enabled: false`.
  Body: `url`: string; `events`: string[]; `description`: string | null; `enabled`: boolean
  Returns: The updated `webhook_endpoint`.
- `DELETE /webhooks/{webhook_id}` — scope `webhooks:manage`. Delete an endpoint. Stops deliveries and deletes the endpoint.
  Returns: The deleted endpoint with `deleted: true`.
- `POST /webhooks/{webhook_id}/test` — scope `webhooks:manage`. Send a test event. Delivers a signed `webhook.test` event now and reports your endpoint's response.
  Returns: `{ object: "webhook_test", ok, status }`
- `GET /webhooks/{webhook_id}/deliveries` — scope `webhooks:manage`. List deliveries. The 50 most recent delivery attempts, with outcome and your endpoint's status code.
  Returns: A list of `webhook_delivery` objects.

## Error codes

`invalid_request` (400, see `details`), `invalid_json` (400), `unauthorized` (401), `insufficient_scope` / `forbidden` (403), `not_found` (404 — also when outside the key's workspace), `conflict` (409), `payload_too_large` (413), `rate_limited` (429), `internal_error` (500), `media_unavailable` (503, retry).

## Limits

25 active keys and 10 webhook endpoints per workspace · join links live up to 7 days · tokens 10 minutes to connect · 300 participants per meeting (500 for a webinar) · 64 KB request bodies · 10 options per poll · 6 GB per recording.
