ENGINEERING NOTES / REAL-TIME VIDEO

Embed Video Calls in Your App: Sign a Token, Redeem an Invite, Join

Add video calls to your product without handing over users or login: the token, invite and join flow, with real code and limits read from the source.

Embed Video Calls in Your App: Sign a Token, Redeem an Invite, Join

To embed video calls in your app, keep three jobs apart: your backend proves who the user is, a call API decides whether that user may join this call, and a media SDK in the client carries the audio and video. In practice that is three moves: your backend signs a short-lived token, your app redeems an invite code with it, and the client connects to the media server with the join details it gets back. Your users, your login and your UI stay yours.

Below is that flow with real code. The worked example is Tele, the calling API we build at CloudGuys. The pattern is the same for any token-based video API, so the design notes still apply if you pick something else. I read everything about Tele here from its source repositories and developer guide on 8 November 2026.

The short answer

  • Your backend holds the API secret and signs a short-lived JWT for each signed-in user. The secret never reaches a browser or a mobile app.
  • Your app redeems an invite code with that token. The call API checks the token, the call’s access policy and the join window, then returns a media URL, a media token and ICE (relay) servers.
  • The client connects with the media SDK for its platform, or hands the join response to a prebuilt call screen.
  • Invite codes are not credentials. A code says which call; the token says who. Both are checked.
  • Plan the edges early: reconnects, a second device, which IP address gets recorded, and who sees host controls.

A link to someone else’s meeting app moves your users into someone else’s product. They sign in there, their identity lives there, and the call record can’t be tied to your user IDs. With an embedded call API, your app is the only place the user ever sees, and every participant is one of your users.

Tele is strict about this. Its guide says “Tele has no anonymous guests”. Every host and participant is identified by your own user_id, which keeps call records, chat history and billing attributable to your accounts. If you need anonymous drop-in guests, weigh that difference before you choose.

How the three moves fit together

The control path (who may join) and the media path (the audio and video) are separate. Your backend and the Tele API decide; the media server only carries the call.

Under the hood, Tele is a Go service in front of a self-hosted LiveKit media server (the repository README says so in its first line), with Cloudflare’s TURN service as the relay for restrictive networks. The practical upshot is that your client uses LiveKit’s open-source client SDKs, so the media side is a documented library you can read, not a proprietary one.

Move 1: your backend signs a user token

Your backend signs an HS256 JWT with your API secret. The token’s iss is your API key, sub is your user ID, and a tele.scope claim says whether it is a user token (for your apps, at most 1 hour) or a service token (for your backend, at most 10 minutes).

Put the signer behind your own authentication, so only a signed-in user can get a token for themselves. With the Go library, tele-go:

import "github.com/cloudguys-io/tele-go/teletoken" // go get github.com/cloudguys-io/[email protected]

var signer = teletoken.Signer{
	APIKey:    os.Getenv("TELE_API_KEY"),
	APISecret: os.Getenv("TELE_API_SECRET"),
}

func teleToken(w http.ResponseWriter, r *http.Request) {
	user := requireSession(r) // your auth
	token, err := signer.UserToken(teletoken.User{ID: user.ID, Name: user.Name, Team: user.Team}, 15*time.Minute)
	if err != nil {
		http.Error(w, "could not sign token", http.StatusInternalServerError)
		return
	}
	json.NewEncoder(w).Encode(map[string]string{"token": token})
}

Python backends can use tele-fastapi, and any JWT library works if you set the claims yourself.

What the token actually contains. On 8 November 2026 I ran the signer from tele-go v0.2.0 locally with a dummy key and secret and decoded the token. Then I asked it for tokens longer than the limits. The output:

header:  {"alg":"HS256","typ":"JWT"}
payload: {"iss":"TKexample","sub":"user-123","exp":1794136300,"nbf":1794135400,"iat":1794135400,"name":"Rahim","team":"support","tele":{"scope":"user"}}
2h user token: teletoken: user token ttl must be in (0, 1h0m0s]
15m service token: teletoken: service token ttl must be in (0, 10m0s]

The library refuses an over-long token before it ever reaches the API, and the server enforces the same limits with 30 seconds of allowed clock skew. A token signed with an unknown key is rejected too: a request to the public API on the same day with a forged token came back 401 with {"error":{"code":"unauthenticated","message":"unknown api key"}}.

Move 2: your app redeems an invite

When a call is created, Tele returns an invite code once (it stores only a hash). Your product owns the link: put the code in a URL or deep link your app understands, such as https://yourapp.example/call/q3V9x2…. When the user opens it, your app fetches a user token from your backend and redeems the code:

const res = await fetch(`${TELE_API}/v1/invites/${encodeURIComponent(code)}/join`, {
  method: "POST",
  headers: { Authorization: `Bearer ${userToken}`, "X-Client-Platform": "web" },
});
const join = await res.json();
if (!res.ok) throw join.error; // { code, message, opens_at? }

The response carries everything the client needs:

{
  "ws_url": "wss://…",
  "token": "eyJhbGciOi…",
  "token_expires_at": "2026-10-13T09:10:00Z",
  "ice_servers": [ { "urls": ["stun:…"] }, { "urls": ["turn:…"], "username": "…", "credential": "…" } ],
  "identity": "u:01a131d1-…",
  "role": "participant",
  "call": { "id": "…", "title": "Quickstart call", "status": "live" }
}

Before the join, your lobby can call GET /v1/invites/{code} to show the title, host and schedule, and whether this user can join now. If not, reason.code tells you what to show: too_early (with opens_at for a countdown), call_ended, not_invited, invite_expired and so on.

Pick the access policy deliberately. listed_only (the default) means the code works only for users you added as members. any_client_user lets any of your users with the code join, so pair it with max_uses and expires_at.

Move 3: the client connects

On the web, pass the join details to livekit-client:

import { Room } from "livekit-client";

const room = new Room();
await room.connect(join.ws_url, join.token, {
  rtcConfig: join.ice_servers ? { iceServers: join.ice_servers } : undefined,
});
if (join.role === "host") showHostControls(); // the host's media token carries room-admin permission

Two details from the guide will save you debugging time. ice_servers replaces the SDK’s defaults, so pass the list as is (it already includes a STUN server), and if it is null, pass nothing. On Android the servers are ignored unless you also set rtcConfig explicitly.

If you would rather not build a call screen, the prebuilt <tele-call> web component takes the same join response and renders the lobby, device pickers, video tiles, screen share, chat and a recording banner:

<script type="module">
  import "@tele/call-ui";
  document.querySelector("tele-call").getJoin = () =>
    fetch("/api/tele/invites/" + code + "/join", { method: "POST" }).then((r) => r.json());
</script>

<tele-call style="height: 100dvh"></tele-call>

Which integration level fits you?

All five libraries are MIT licensed and install from tagged releases on GitHub (checked 8 November 2026).

You want Use What you still build
Full control of the UI on any platform REST API plus the LiveKit client SDK for your platform Token endpoint, lobby, call screen
A finished call screen in any web app tele-ui-pack (<tele-call> and a React wrapper) Token endpoint and a join route
Next.js App Router pages tele-nextjs-ui on top of <tele-call> A backend library for the routes
Flutter on iOS, Android and web tele-flutter (TeleCallScreen) A token provider
A Go or Python backend tele-go or tele-fastapi: signer, typed client, webhook verification Your own auth in front of it

The backend libraries and the UI kits share one set of default routes under /api/tele (token, invite preview, invite join, call join, chat history), so a Go or Python backend works with the web and Flutter kits without configuration.

The limits, read from the code

I built this table from the Tele server source and the libraries on 8 November 2026. Design your UI around these numbers.

Limit Value Where it is enforced
User token lifetime at most 1 hour server/internal/auth/auth.go, and the signer
Service token lifetime at most 10 minutes same
Clock skew allowed 30 seconds auth.go
Media token to connect 10 minutes (the SDK refreshes it while connected) server/internal/config/config.go
Participants per call 2 to 10, default 10 database check constraint and config
Default maximum call length 4 hours from the first join database default
Join requests 20 per minute per user, then 429 rate_limited join handler
Recording off unless you set "record": true or start it mid-call record boolean NOT NULL DEFAULT false
IP address and user agent erased after 30 days IP_RETENTION default 720h

Tele is built for one-to-one and small group calls. It is not a webinar or large-meeting product.

Edge cases to handle before launch

  1. Reconnects. The media SDK reconnects after short network blips. If it gives up, call the join endpoint again with a fresh user token.
  2. A second device. When the same user joins from another device, the first is disconnected with DUPLICATE_IDENTITY. Show “you joined elsewhere”, not an error.
  3. Which IP gets recorded. Tele records the address that sends the join request. If your backend joins on a user’s behalf, use a service token and pass the user’s IP and user agent in the body; otherwise your server’s IP lands in the call record. If you sit behind a proxy, trust X-Forwarded-For only from that proxy (we wrote up how to do that safely in Go).
  4. Rejoins. After the first invite join the user is a member, so reconnect by call ID (POST /v1/calls/{id}/join), not by burning another use of a limited invite.
  5. Host controls. Show “End call” only when role is host.

After the call, your backend can read the session record (join and leave times, platforms, peak and unique participants, billed room-minutes) or receive signed webhooks such as call.ended and recording.ready. Tele bills room-minutes: the time a room is in use, not multiplied by the number of people in it.

Where Tele does not fit

We make Tele, so you should hear from us where to look elsewhere. Calls are capped at 10 participants, and there are no anonymous guests. The prebuilt call screen is web only; native apps use tele-flutter or the platform’s LiveKit SDK. We have not published latency benchmarks, uptime commitments or a price list. The managed workspace is free for organisations of up to ten people, and other paths start with a conversation. If you need large meetings, broadcast or a fully self-run open-source stack, a raw SFU or a complete meeting server may suit you better.

FAQ

Do I need to run my own media server? Not to use Tele. It runs the LiveKit media server for you, and your client uses LiveKit’s open-source client SDKs.

Can anonymous guests join a call? No. Every host and participant is identified by your own user_id.

How many people can be in one call? Between 2 and 10, with a default of 10.

How long does a token last? A user token for your apps lasts at most 1 hour, and a service token for your backend at most 10 minutes. Media tokens last 10 minutes and the SDK refreshes them while connected.

Are calls recorded by default? No. Recording is off unless you set "record": true or start it mid-call.

Try it

Sign in to the managed workspace with Google, and an organisation owner or admin can create up to two API keys under the workspace’s developer settings. Then follow the quickstart to make a call with curl in about ten minutes. The Tele product page has the overview, and you can ask us about a private deployment at [email protected].

BUILT AND OPERATED BY CLOUDGUYS

Tele: audio and video calls
Real-time audio and video calls, running where you choose.

Embed calls in your own application through the meeting API, or give your team a ready workspace with scheduling, direct messages and optional recordings.

Visit Tele ↗ About Tele ↗
Prefer a text version?Read this page as Markdown · All pages