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

# Your first video meeting in 10 minutes

> Create a video room, invite a guest, join with a token, and find the recording afterwards — one copy-paste loop through the Orbit video API, with the matching dashboard steps.

Orbit's video surface covers scheduled meetings, persistent rooms, webinars, recording egress, and embeddable join buttons. This tutorial is the shortest path through the core loop: **create a room, invite a guest, join, end, and find the recording**. Every step is a single `curl` you can paste into a terminal, with the equivalent dashboard click-path alongside.

By the end you will have hosted a two-person recorded meeting and know which deep guide to read for each capability you saw.

Here is the loop:

* Create a recorded room with one `curl` — or the equivalent dashboard clicks
* Issue a guest invite, delivered by email or shared as a link
* Mint a join token from your backend and join as the host
* End the meeting and find the recording
* Follow the deep guide for each capability you just saw

## Before you start

* An Orbit account with video enabled on your plan, and an API key (`dv_live_sk_…` for production, `dv_test_sk_…` for test mode). Create keys under **Settings → API keys**.
* A basic grasp of how you authenticate: every call below sends your key in the `X-API-Key` header.
* If you want to experiment without touching live data, run these same calls in test mode first — see [Sandbox test mode](/guides/sandbox-test-mode).

Export your key once so the snippets below run as-is:

```bash theme={null}
export ORBIT_API_KEY="dv_test_sk_…"
```

The snippets use the test key; swap in your live key when you repeat the loop in production.

## 1. Create the room

A room is a scheduled meeting: a name, an optional start time, and a recording policy. This call turns recording on so step 4 has an artifact to find:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/video/rooms-scheduled" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Weekly customer sync",
    "recording_enabled": true,
    "recording_format": "composite"
  }'
```

Response (201):

```json theme={null}
{
  "data": {
    "id": "3f8a1c2e-9b4d-4e6f-8a21-7c0d5e9f4b31",
    "name": "Weekly customer sync",
    "status": "scheduled",
    "recording_enabled": true,
    "recording_format": "composite"
  }
}
```

Store `id` — the invite and join calls take it as `:id` in the path.

`recording_format` picks the artifact shape: `composite` renders one MP4 with every track mixed (the right default for a first meeting), `tracks` writes per-participant tracks for analysis or QA review, and `both` produces both families. When omitted, your organization's default applies.

**In the dashboard:** open **Voice → Video**, click **Schedule** (or **Create room** for an ad-hoc meeting), set the name and flip recording on. The room appears in the list with its id and join link — the same row this API call returns.

Omit `scheduled_at` and the room is available immediately; pass an ISO timestamp (e.g. `"2026-10-14T15:00:00Z"`) to hold it for a future slot.

Two people is the floor, not the default size: `max_participants` caps the room from 2 up to 300 seats (bounded by your plan), and omitting it inherits your plan's ceiling. For a series rather than a one-off, pass `recurrence_rule` as an RFC 5545 RRULE body (e.g. `FREQ=WEEKLY;BYDAY=MO;COUNT=10`) and the invite emails carry calendar attachments for the whole series. Capacity, recurrence, and reminder settings are covered in [Video meetings](/guides/video-meetings).

If a field fails validation the API answers `422` with a `VALIDATION_ERROR` code and the offending field in the details block — match the field name against the body above and re-send. A `401` means the `X-API-Key` header is missing or the key was revoked; a `403` means the key's role does not cover video writes.

Read the room back any time to confirm its current status, recording policy, and seat cap:

```bash theme={null}
curl -s "https://api.orbit.devotel.io/api/v1/video/rooms-scheduled/$ROOM_ID" \
  -H "X-API-Key: $ORBIT_API_KEY" | jq '.data.status'
```

`GET /api/v1/video/rooms-scheduled?status=scheduled&keyword=sync` searches the catalog by name — handy once the id has scrolled out of your shell history.

## 2. Invite a guest

Guests join through an invite: a bounded token with an expiry and a use-count, plus a ready-to-share link. Issue one and have the API email it, or hand the link over yourself:

```bash theme={null}
ROOM_ID="3f8a1c2e-9b4d-4e6f-8a21-7c0d5e9f4b31"

curl -X POST "https://api.orbit.devotel.io/api/v1/video/rooms-scheduled/$ROOM_ID/invites" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "guest_name": "Priya Sharma",
    "guest_email": "priya@example.com",
    "expires_in_hours": 24,
    "max_uses": 1
  }'
```

Response (201):

```json theme={null}
{
  "data": {
    "id": "8c2d4e6f-1a3b-4c5d-9e8f-2a4b6c8d0e12",
    "room_id": "3f8a1c2e-9b4d-4e6f-8a21-7c0d5e9f4b31",
    "invite_token": "vinv_9f1e2d3c4b5a6789",
    "participant_name": "Priya Sharma",
    "expires_at": "2026-10-14T09:05:00.000Z",
    "max_uses": 1,
    "uses_count": 0,
    "join_url": "https://orbit.devotel.io/en/join/vinv_9f1e2d3c4b5a6789",
    "email_status": "sent"
  }
}
```

* `join_url` is the link your guest opens. No account, no API key — the invite is their credential.
* `email_status` tells you whether Orbit dispatched the invite email (`sent`, `failed`, or `not_requested` when you omit `guest_email`). On `failed` the invite itself still works; hand over `join_url` manually.
* `max_uses: 1` makes the link single-entry; raise it for a link a guest reuses across the day, or omit it for the default.

`guest_name` and `participant_name` are accepted aliases for the same field — the name the guest's tile shows in the room. Use one or the other; if both are present, `participant_name` wins.

**The guest experience:** opening `join_url` lands on a lobby page that asks for a name (pre-filled when you passed `guest_name`), requests camera and microphone permission, and joins as a participant. Guests see the same room controls — mute, camera, screen share, chat — without ever touching the dashboard.

Prefer to share the link yourself? Omit `guest_email` and copy `join_url` into any channel. Revoking is a one-click action on the room's invite list in the dashboard, or the revoke endpoint — the full invite lifecycle, including listing and expiring invites, is in [Video room access tokens](/guides/video-room-access-tokens).

Use invites for people you do not authenticate — guests, customers, candidates — and `/join` for users your system already knows, where your backend should own identity and role. The two coexist in one room: a panel on minted tokens plus an audience on invite links is the standard webinar layout.

For embedded apps that want the guest flow server-side, `POST /api/v1/video/invites/:invite_token/redeem` exchanges an invite for a join token programmatically — the same token the `join_url` lobby mints when the guest clicks through.

## 3. Get a join token and join

Your own backend joins authenticated users by minting a join token. The token carries the participant's identity and role; the room URL is where the browser goes, the token is what authorizes the connection:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/video/rooms-scheduled/$ROOM_ID/join" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "identity": "acct-4821",
    "display_name": "Alex Host",
    "role": "host"
  }'
```

Response (200):

```json theme={null}
{
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "token_type": "livekit_access_token",
    "livekit_url": "wss://…",
    "room_sid": "VR_8e2f4a1c…",
    "identity": "acct-4821"
  }
}
```

* `token` is a short-lived, identity-bound access token. Your server mints it; your client consumes it. Never embed an API key in browser code to work around this step. Pass `ttl_seconds` (60 to 86400) to override the token's validity window; expiry is enforced at the SFU, so a stale token refuses the connection rather than joining silently.
* `token_type` and `livekit_url` tell a client SDK how to connect: feed `token` to the LiveKit client and point it at `livekit_url`. Browser joins via the room link (your dashboard room row, or the `join_url` from step 2) need no token handling at all.
* Requesting `role: "host"` grants moderator controls (mute-all, lock, recording start/stop). Everyone else joins as `"participant"`.

The `role` field is the simple split — `host` moderates, `participant` doesn't. When you need finer grain, `participant_tier` on the same call selects one of four permission tiers:

| Tier | Publishes media | Visible to others | Use for |
| - | - | - | - |
| `viewer` | No | Yes | Audience members, observers |
| `panelist` | Yes | Yes | Speakers without moderator rights |
| `host` | Yes | Yes | Moderators (owner/admin callers only) |
| `hidden_supervisor` | No | No | QA, compliance, silent supervisors |

Minting `host` or `hidden_supervisor` requires an owner/admin credential; a `403` on the join call means the role or tier exceeded what the caller may grant.

With both legs connected — your host session and the guest's invite link — the meeting is live.

Roles, permission tiers, per-participant publish limits, and token TTLs are covered in depth in [Video room access tokens](/guides/video-room-access-tokens).

## 4. End the meeting and find your recording

End the meeting from the room controls (host) or let the room close when everyone leaves. Because you set `recording_enabled: true` at create time, Orbit's media pipeline rendered the session while it ran; a few minutes after the room ends, the artifact is attached to the room.

**In the dashboard:** open **Voice → Video → History**, pick the room, and the recording row shows playback, download, and share actions. The share player gives you a link you can send to anyone in your organization without exporting the file.

Recordings land in Orbit-managed storage by default; the share player and download links in History point there. If your compliance posture requires recordings never to touch shared infrastructure, connect your own bucket first per [Recordings to your own storage](/guides/video-recording-byo-storage) — the same API flow then delivers artifacts to your bucket instead.

While the meeting runs, the host console covers moderation in one place: mute-all, lock the room, spotlight a presenter, promote a participant to panelist, and turn on live captions. Every console action has a matching `POST /api/v1/video/rooms-scheduled/$ROOM_ID/…` call (`/mute-all`, `/lock`, `/spotlight`) for backend-driven moderation.

Two guides cover what happens next, depending on where recordings should live:

* [Share recordings with the hosted player](/guides/video-recording-share-player) — hosted playback links, expiry, and access control.
* [Recordings to your own storage](/guides/video-recording-byo-storage) — deliver the composite MP4 or per-track files into your own bucket the moment egress finishes.

The host also controls recording from the room controls while the meeting runs: start late, stop early, and exclude an individual participant from the recording artifact. In two-party-consent regions, pair that with per-participant consent captured at join time — both are covered in [Video room access tokens](/guides/video-room-access-tokens).

If you need per-participant tracks instead of one mixed MP4, that is the `recording_format: "tracks"` option from step 1 — re-create the room with it and the same loop produces separable tracks.

If your backend should act the moment a recording lands (notify participants, attach the file to a CRM record), subscribe to the `video.room.ended` and `video.recording.completed` webhook events under **Settings → Webhooks** instead of polling — the payload shapes are in the [Webhook events reference](/reference/webhook-events).

## 5. Run the whole loop as one script

Once you have walked the steps once, this script runs create → invite → join in a single paste against test mode. Two interactive steps stay human: the guest opens `join_url` in a browser, and the meeting ends when participants leave.

```bash theme={null}
set -euo pipefail

# 1. Create a recorded room; capture its id.
ROOM_ID=$(curl -s -X POST "https://api.orbit.devotel.io/api/v1/video/rooms-scheduled" \
  -H "X-API-Key: $ORBIT_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"Quickstart sync","recording_enabled":true,"recording_format":"composite"}' \
  | jq -r '.data.id')
echo "room: $ROOM_ID"

# 2. Issue a guest invite and print the share link.
curl -s -X POST "https://api.orbit.devotel.io/api/v1/video/rooms-scheduled/$ROOM_ID/invites" \
  -H "X-API-Key: $ORBIT_API_KEY" -H "Content-Type: application/json" \
  -d '{"guest_name":"Priya Sharma","expires_in_hours":24,"max_uses":1}' \
  | jq -r '"guest link: \(.data.join_url)"'

# 3. Mint a host join token (the value a LiveKit client connects with).
curl -s -X POST "https://api.orbit.devotel.io/api/v1/video/rooms-scheduled/$ROOM_ID/join" \
  -H "X-API-Key: $ORBIT_API_KEY" -H "Content-Type: application/json" \
  -d '{"identity":"quickstart-host","display_name":"Quickstart Host","role":"host"}' \
  | jq -r '"join: token_type=\(.data.token_type) room=\(.data.room_sid)"'
```

Verify the meeting afterwards end-to-end: the room row shows status `ended` under **Voice → Video → History**, the recording appears on it a few minutes later, and `GET /api/v1/video/rooms-scheduled/$ROOM_ID/activity` returns the join/leave timeline for the session. Rooms move through `scheduled` → `live` → `ended`, and the same three statuses filter `GET /api/v1/video/rooms-scheduled?status=…` when your backend lists rooms programmatically.

## 6. Where to go next

You have run the full loop: room, invite, join, recording. The deep guides per capability:

* [Video meetings](/guides/video-meetings) — the complete scheduled-meeting reference: recurrence, reminders, co-hosting, and the meetings console.
* [Persistent rooms](/guides/video-persistent-rooms) — always-on rooms with a stable URL for office hours and standing meetings.
* [Webinars](/guides/video-webinars) — panelist/audience splits at higher seat counts, with a [registration gate](/guides/video-webinar-registration-gate) for sign-up flows.
* [Embed a consultation button](/guides/embed-video-consultation-button) — put a one-click video call button on your website.
* [Room access tokens](/guides/video-room-access-tokens) — the full token and invite contract, including revocation and TTL policy.
* [Dial-in and SIP room systems](/guides/video-room-dial-in-and-sip-room-system) — a PSTN number and PIN for participants who join by phone.
* [HLS/DASH egress](/guides/video-room-hls-dash-egress) — live-stream the room out for large read-only audiences; [RTMP egress](/guides/video-room-ingress-whip-rtmp) sends it to external platforms.

And when something misbehaves, the common culprits:

| Symptom | Cause | Fix |
| - | - | - |
| `401` on every call | Missing or wrong `X-API-Key` | Check the key prefix: `dv_test_sk_` vs `dv_live_sk_` |
| `403` minting a host token | Key/user lacks the host role for that room | Join as `participant`, or use an owner/admin credential for `role: "host"` |
| Guest link dead | `expires_at` passed or `max_uses` exhausted | Issue a fresh invite; raise `expires_in_hours`/`max_uses` |
| No recording after the meeting | `recording_enabled` was false at create time | Recording is a create-time setting; re-create the room with it on |
| `429` on any call | Key-level rate limit | Honour `Retry-After`; batch setup calls rather than retrying in a loop |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.