Skip to content

Start/Your first room

Your first room

Create a room, mint a token for one identity and one role, join from the browser, then read back who was in the call. Four requests, and only one of them runs in a browser.

Create a room

A room is the space participants connect into. Create it from your server with your project API key. The name must match [a-zA-Z0-9_-]{1,128} and is unique within the project.

curl
curl -X POST https://api.usevelo.xyz/v1/rooms \
  -H "Authorization: Bearer $VELO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "demo-room", "max_participants": 4}'

The room comes back with the identifiers the rest of the API uses. If the project has a default template, the room is attached to it, which is what makes roles available in the next step.

json201 Created
{
  "id": "6f1d0a58-2f0e-4d1a-9a3f-5c0b8a6d2e11",
  "project_id": "prj_0a1b2c3d4e5f",
  "name": "demo-room",
  "namespaced_name": "prj_0a1b2c3d4e5f.demo-room",
  "max_participants": 4,
  "empty_timeout_seconds": 300,
  "metadata": {},
  "status": "created",
  "template_id": "tpl_0123456789ab",
  "created_at": "2026-07-28T09:12:44Z"
}

Mint a token

A token admits one identity to one room until it expires. Naming a role hands the decision about what that participant may do to the template, so nothing about permissions is written in your application code.

curl
curl -X POST https://api.usevelo.xyz/v1/tokens \
  -H "Authorization: Bearer $VELO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "room": "demo-room",
    "identity": "dr-amina",
    "role": "practitioner",
    "ttl_seconds": 3600
  }'

The response carries token, the url to connect to, expires_at, the role you asked for and a publish_profile. The SDK applies its resolution and bitrate hints when publishing. The media server enforces the grants inside the token and measures the total published bitrate against publish_profile.max_publish_kbps, reporting sustained overage as a participant.publish_limit_exceeded webhook. Every other hint is advisory.

A client token never carries room admin. Ending a room, removing a participant, muting someone else and changing a role are moderation calls, authorised by your project API key from your server or by a room token whose role holds that permission.

The same request through the SDK, as a route handler that returns only the two fields a browser needs.

typescriptserver
import { VeloClient } from "@usevelo/client";

const velo = new VeloClient({
  baseUrl: "https://api.usevelo.xyz",
  apiKey: process.env.VELO_API_KEY as string,
});

const token = await velo.createToken({
  room: "demo-room",
  identity: "dr-amina",
  role: "practitioner",
  ttlSeconds: 3600,
});

return Response.json({ token: token.token, url: token.url });

Join from the browser

bash
npm install @usevelo/client livekit-client

connectToRoom is the browser-safe half of @usevelo/client. It takes the token and url from step 2 and refuses anything that starts with vk_, so a project key cannot reach a browser by accident.

typescriptbrowser
import { connectToRoom, RoomEvent } from "@usevelo/client";

const { token, url } = await fetch("/api/velo-token").then((response) => response.json());

const room = await connectToRoom({ token, url });

room.on(RoomEvent.TrackSubscribed, (track) => {
  document.querySelector("#stage")?.append(track.attach());
});

await room.localParticipant.setMicrophoneEnabled(true);
await room.localParticipant.setCameraEnabled(true);

track.attach() returns a media element you can put anywhere in the page. Everything below this line is livekit-client, re-exported unchanged.

Watch the session

Every join and leave is recorded as a participant session. Read them back to see who is in the room right now, or what happened in a call that already ended.

curl
curl -G https://api.usevelo.xyz/v1/sessions \
  -H "Authorization: Bearer $VELO_API_KEY" \
  -d room=demo-room \
  -d active=true
json200 OK
{
  "sessions": [
    {
      "id": "0d9a4c1e-7b25-4a1f-8f2c-9b6d3a1f0e77",
      "room": "demo-room",
      "participant_identity": "dr-amina",
      "participant_sid": "PA_9xKq4mTb2LwR",
      "role": "practitioner",
      "joined_at": "2026-07-28T09:14:02Z",
      "duration_seconds": 184,
      "track_count": 2
    }
  ],
  "limit": 50,
  "offset": 0,
  "has_more": false
}

A session carries the role the participant joined as, captured at join time, and how many tracks they published. GET /v1/sessions/{id} adds the individual tracks, which answers whether a camera was ever on without replaying anything.

  • active=true limits the page to sessions that have not ended.
  • room, identity and role filter by exact value.
  • after and before take RFC 3339 timestamps.

What Free allows

A new project starts on Free, which carries everything you just did and is enough to build against. What it does not carry is recording and live streaming. Those are not degraded on Free, they are refused: starting a recording, starting a broadcast, opening an ingress or adding a destination all answer 402 feature_not_in_plan, with field naming which of the two you asked for. Plan for that before you build a call screen with a record button on it.

  • 2 rooms at once. A third returns 402 room_limit_reached. Ended rooms do not count, so end them when a call finishes rather than leaving them open.
  • 5 people per room.
  • 1,000 participant-minutes a month. Spending them returns 402 quota_exceeded on new rooms, tokens and recordings until the month rolls over.
  • No recording, no live streaming. Starter is the first plan that includes both.

Every one of these refusals is a 402 with a message that says which plan you are on and what it allows. None of them are silent, and none of them degrade quality to stay inside a limit. See Pricing for what each plan carries.

Token parameters

The full body accepted by POST /v1/tokens.

POST /v1/tokens request body
FieldTypeRequiredDescription
roomstringYesName of an existing room in this project. Must match [a-zA-Z0-9_-]{1,128}.
identitystringYesWho the token admits. Must match [a-zA-Z0-9_.@-]{1,128}.
rolestringNoA role in the room's template. When present it decides every permission and permissions is ignored.
ttl_secondsintegerNoClamped to 60 seconds minimum and 24 hours maximum. Defaults to one hour.
namestringNoDisplay name carried on the participant.
metadatastringNoOpaque string handed to every other participant in the room.
permissionsobjectNocan_publish, can_subscribe and can_publish_data, each defaulting to true. Only read when no role is named.

Roles are the reason this endpoint stays this small. Templates and roles covers what a role can fix.

Was this page useful?

Edit this page on GitHub