Join links
A join link lets one of your users into one meeting — no GurukulLive account, password or e-mail needed. Your backend mints it with an API key at the moment the user clicks Join.
Create a join link
POST /api/v1/public/meetings/:id/join-links — scope meetings:write. :id is the meeting'sGurukulLive id or your externalId.
curl -X POST https://gurukullive.example.com/api/v1/public/meetings/algebra-10a-2026-10-14/join-links \
-H "Authorization: Bearer $VMEET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"externalUserId":"student-1001","role":"PARTICIPANT","displayName":"Aarav P.","expiresInSeconds":300,"singleUse":true,"redirectUrl":"https://lms.example.com/classes/algebra"}'| Field | Meaning |
|---|---|
userId | externalUserId | Who the link is for (one of them is required). The user must be an active member of your organization. |
role | HOST, COHOST, PARTICIPANT (default) or VIEWER (watch-only: no camera, microphone or screen share). The role is capped by the user's org role — a student only becomes host when the key has meetings:write and you ask for it explicitly. |
displayName | Name shown in the room (defaults to the user's full name). |
expiresInSeconds | Default 300, maximum 86 400. Mint links just in time rather than storing them. |
singleUse | Default true: the link works once. Reloading the page keeps working in the same browser tab. |
redirectUrl | https URL the join page sends the user to after they leave or the meeting ends. |
{
"data": {
"url": "https://gurukullive.example.com/j/vjt_9Qm…",
"embedUrl": "https://gurukullive.example.com/embed/meeting/algebra-10a-k3f9q2?jt=vjt_9Qm…",
"token": "vjt_9Qm…",
"expiresAt": "2026-10-14T09:35:00.000Z",
"role": "PARTICIPANT",
"singleUse": true,
"userId": "cm2u…"
}
}Use the link
url— full-page join. Redirect the browser, open it in a new tab, or open it in an Android Custom Tab / iOSSFSafariViewController. The page shows a camera/mic check, then the class; after leaving it goes toredirectUrl.embedUrl— inside your page. Iframe it with the JS SDK to get events (joined, left, hand raised …) and commands (mute, leave …).
The token is exchanged by JavaScript after the page loads, so link previews in chat apps (which only fetch the HTML) never use up a single-use link.
Access rules still apply
When the link is opened, GurukulLive checks again and refuses with a clear message when:
- the link expired (
join_token_expired), was already used (join_token_used) or revoked (join_token_revoked); - the user was deactivated or removed from the organization (
user_deactivated,not_a_member); - the meeting is
ENROLLED/INVITED-only and the user is not enrolled/invited (not_enrolled,not_invited); - the meeting has ended or was cancelled (
meeting_ended), is locked or full, or a plan limit is reached.
If the meeting has a waiting room, the user waits on a “waiting for the host” screen until a host admits them — the page updates by itself.
Native apps
- Simplest: open
urlin a Custom Tab (Android) orSFSafariViewController(iOS). Camera, microphone and audio work like in the browser, and yourredirectUrlcan be an app link back into your app. - Inside your UI: load
urlorembedUrlin a WebView — see mobile WebViews for the required settings and the native event bridge. - GurukulLive Android app: deep-link to
vmeet://join?token=<token>; the app exchanges the token itself. Use the token only once — either the deep link or the web page.
The exchange (for custom clients)
GurukulLive's pages do this for you. If you build your own client on the GurukulLive media stack, exchange the token yourself — the endpoint is public, the token is the credential:
POST /api/v1/join-tokens/exchange
Content-Type: application/json
{ "token": "vjt_9Qm…", "deviceId": "a-stable-per-device-id" }{
"data": {
"accessToken": "eyJhbGciOi…", // meeting session, Authorization: Bearer on this meeting's routes
"tokenType": "Bearer",
"expiresAt": "2026-10-14T11:30:00.000Z",
"livekitToken": "eyJhbGciOi…", // null while waiting in the waiting room
"livekitUrl": "wss://gurukullive.example.com/api/v1/livekit-sfu",
"role": "PARTICIPANT",
"waitingRoom": null, // { queueId, status } when the user must be admitted first
"meeting": {
"id": "cm2x8k3…", "kind": "MEETING", "slug": "algebra-10a-k3f9q2", "title": "Algebra · Grade 10 A",
"status": "LIVE", "orgId": "org_123",
"settings": { "muteOnEntry": true, "lectureMode": false, "allowStudentScreenShare": false, "recordAutomatically": true }
},
"user": { "id": "cm2u…", "externalId": "student-1001", "displayName": "Aarav P." },
"redirectUrl": "https://lms.example.com/classes/algebra"
}
}- Keep the
accessTokenin memory. It is accepted only on that meeting's routes (/api/v1/meetings/:slug/…, or/api/v1/orgs/:orgId/meetings/:id/…for class sessions) and refreshed withPOST /api/v1/join-tokens/refresh(Bearer) for up to 24 hours after the exchange. - Waiting room: poll
GET /api/v1/meetings/:slug/waiting-room/status/:queueId; onceADMITTED, get the room token withPOST /api/v1/meetings/:slug/tokenand{ queueId, deviceId }. - The exchange is rate limited per IP (300/min) — generous enough for a whole class behind one school network.
Security notes
- Never put an API key in a page or app. Mint links on your server, per user, per click.
- Treat link URLs like passwords until they are used: don't log them or send them to analytics.
- Deactivating a user (
POST /public/users/:id/deactivate) revokes their links and disconnects them.