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 -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.
{
"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 -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.
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
npm install @usevelo/client livekit-clientconnectToRoom 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.
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 -G https://api.usevelo.xyz/v1/sessions \
-H "Authorization: Bearer $VELO_API_KEY" \
-d room=demo-room \
-d active=true{
"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=truelimits the page to sessions that have not ended.room,identityandrolefilter by exact value.afterandbeforetake 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_exceededon 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.
| Field | Type | Required | Description |
|---|---|---|---|
| room | string | Yes | Name of an existing room in this project. Must match [a-zA-Z0-9_-]{1,128}. |
| identity | string | Yes | Who the token admits. Must match [a-zA-Z0-9_.@-]{1,128}. |
| role | string | No | A role in the room's template. When present it decides every permission and permissions is ignored. |
| ttl_seconds | integer | No | Clamped to 60 seconds minimum and 24 hours maximum. Defaults to one hour. |
| name | string | No | Display name carried on the participant. |
| metadata | string | No | Opaque string handed to every other participant in the room. |
| permissions | object | No | can_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?