---
title: "Embed Video Calls in Your App: Token, Invite, Join | CloudGuys Blog"
description: "Add video calls to your product in three moves: your backend signs a short-lived token, your app redeems an invite, the client joins. Real code and limits."
url: "https://cloudguys.io/blog/embed-video-calls-in-your-app"
language: "en"
image: "https://cloudguys.io/_astro/cover.CEv9kM-S.jpg"
published: "2026-11-11T00:00:00.000Z"
modified: "2026-11-11T00:00:00.000Z"
---

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.

Sabbir Ahmed · · · November 11, 2026 · · · 9 min read · · · Facts checked November 8, 2026 · · · About Tele

![Embed Video Calls in Your App: Sign a Token, Redeem an Invite, Join](https://cloudguys.io/_astro/cover.CEv9kM-S_1cRMO3.webp)

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](https://tele.cloudguys.io/docs/) 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.

## Why not just embed a meeting link?

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](https://github.com/livekit/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](https://github.com/cloudguys-io/tele-go):

```go
import "github.com/cloudguys-io/tele-go/teletoken" // go get github.com/cloudguys-io/tele-go@v0.2.0

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](https://github.com/cloudguys-io/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:

```text
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:

```ts
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:

```json
{
  "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`:

```ts
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:

```html
<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](https://github.com/cloudguys-io/tele-ui-pack) (`<tele-call>` and a React wrapper) | Token endpoint and a join route |
| Next.js App Router pages | [tele-nextjs-ui](https://github.com/cloudguys-io/tele-nextjs-ui) on top of `<tele-call>` | A backend library for the routes |
| Flutter on iOS, Android and web | [tele-flutter](https://github.com/cloudguys-io/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](https://cloudguys.io/blog/real-client-ip-behind-istio-ambient-gateway-secure-x-forwarded-for-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](https://tele.cloudguys.io/docs/quickstart/) to make a call with `curl` in about ten minutes. The [Tele product page](https://cloudguys.io/products/tele) has the overview, and you can ask us about a private deployment at [hello@cloudguys.io](mailto:hello@cloudguys.io).

#Video Call API · #WebRTC · #LiveKit · #Tele · #Embedded Video

Share this article

-   [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcloudguys.io%2Fblog%2Fembed-video-calls-in-your-app%3Futm_source%3Dlinkedin%26utm_medium%3Dsocial "Share on LinkedIn")
-   [X](https://twitter.com/intent/tweet?text=Embed%20Video%20Calls%20in%20Your%20App%3A%20Sign%20a%20Token%2C%20Redeem%20an%20Invite%2C%20Join&url=https%3A%2F%2Fcloudguys.io%2Fblog%2Fembed-video-calls-in-your-app%3Futm_source%3Dx%26utm_medium%3Dsocial "Share on X")
-   [Reddit](https://www.reddit.com/submit?url=https%3A%2F%2Fcloudguys.io%2Fblog%2Fembed-video-calls-in-your-app%3Futm_source%3Dreddit%26utm_medium%3Dsocial&title=Embed%20Video%20Calls%20in%20Your%20App%3A%20Sign%20a%20Token%2C%20Redeem%20an%20Invite%2C%20Join "Share on Reddit")
-   [Hacker News](https://news.ycombinator.com/submitlink?u=https%3A%2F%2Fcloudguys.io%2Fblog%2Fembed-video-calls-in-your-app%3Futm_source%3Dhackernews%26utm_medium%3Dsocial&t=Embed%20Video%20Calls%20in%20Your%20App%3A%20Sign%20a%20Token%2C%20Redeem%20an%20Invite%2C%20Join "Share on Hacker News")
-   [Email](mailto:?subject=Embed%20Video%20Calls%20in%20Your%20App%3A%20Sign%20a%20Token%2C%20Redeem%20an%20Invite%2C%20Join&body=Embed%20Video%20Calls%20in%20Your%20App%3A%20Sign%20a%20Token%2C%20Redeem%20an%20Invite%2C%20Join%0A%0Ahttps%3A%2F%2Fcloudguys.io%2Fblog%2Fembed-video-calls-in-your-app%3Futm_source%3Demail%26utm_medium%3Dsocial "Share on Email")

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 ↗](https://tele.cloudguys.io/?utm_source=cloudguys_blog&utm_medium=referral&utm_campaign=embed-video-calls-in-your-app) [About Tele ↗](https://cloudguys.io/products/tele)

## Structured data

```json
[
  {
    "@context": "https://schema.org",
    "@type": [
      "ProfessionalService",
      "Organization"
    ],
    "@id": "https://cloudguys.io/#organization",
    "name": "CloudGuys",
    "url": "https://cloudguys.io/",
    "logo": {
      "@type": "ImageObject",
      "url": "https://cloudguys.io/assets/logo.png",
      "width": 512,
      "height": 512
    },
    "image": "https://cloudguys.io/assets/og-image.png",
    "description": "Self-hosted software products, senior engineers embedded in client teams, and cloud security and architecture advisory across AWS, Google Cloud, Azure, Kubernetes, and infrastructure as code.",
    "email": "hello@cloudguys.io",
    "founder": {
      "@type": "Person",
      "@id": "https://cloudguys.io/authors/sabbir-ahmed#person",
      "name": "Sabbir Ahmed",
      "url": "https://cloudguys.io/authors/sabbir-ahmed"
    },
    "slogan": "Ship with confidence. Keep control.",
    "areaServed": "Worldwide",
    "hasOfferCatalog": {
      "@type": "OfferCatalog",
      "name": "CloudGuys engagements",
      "itemListElement": [
        {
          "@type": "Offer",
          "itemOffered": {
            "@type": "Service",
            "name": "Embedded engineers",
            "url": "https://cloudguys.io/services/embedded-engineers",
            "provider": {
              "@id": "https://cloudguys.io/#organization"
            }
          }
        },
        {
          "@type": "Offer",
          "itemOffered": {
            "@type": "Service",
            "name": "Cloud security assessment",
            "url": "https://cloudguys.io/services/cloud-security-assessment",
            "provider": {
              "@id": "https://cloudguys.io/#organization"
            }
          }
        },
        {
          "@type": "Offer",
          "itemOffered": {
            "@type": "Service",
            "name": "Cloud architecture review",
            "url": "https://cloudguys.io/services/cloud-architecture-review",
            "provider": {
              "@id": "https://cloudguys.io/#organization"
            }
          }
        },
        {
          "@type": "Offer",
          "itemOffered": {
            "@type": "Service",
            "name": "Security remediation",
            "url": "https://cloudguys.io/services/security-remediation",
            "provider": {
              "@id": "https://cloudguys.io/#organization"
            }
          }
        }
      ]
    },
    "knowsAbout": [
      "DevOps",
      "Kubernetes",
      "Terraform",
      "Cloud migration",
      "Cloud cost optimization",
      "GitOps",
      "Site reliability engineering",
      "Kubernetes dashboard",
      "AI SRE",
      "Kubernetes troubleshooting",
      "AWS",
      "Google Cloud",
      "Azure"
    ],
    "contactPoint": {
      "@type": "ContactPoint",
      "contactType": "sales",
      "email": "hello@cloudguys.io",
      "availableLanguage": [
        "English"
      ],
      "areaServed": "Worldwide"
    },
    "sameAs": [
      "https://www.linkedin.com/company/agrohitech",
      "https://berth.cloudguys.io",
      "https://tele.cloudguys.io"
    ],
    "owns": [
      {
        "@type": "SoftwareApplication",
        "@id": "https://berth.cloudguys.io/#software",
        "name": "Berth",
        "applicationCategory": "DeveloperApplication",
        "operatingSystem": "Kubernetes",
        "url": "https://berth.cloudguys.io",
        "description": "Berth is a self-hosted Kubernetes dashboard with a read-only AI SRE: endpoint health, capacity planning, guided app exposure, and evidence-based troubleshooting, run in your own cluster."
      },
      {
        "@type": "SoftwareApplication",
        "@id": "https://tele.cloudguys.io/#software",
        "name": "Tele",
        "applicationCategory": "CommunicationApplication",
        "operatingSystem": "Web",
        "url": "https://tele.cloudguys.io",
        "description": "Tele is real-time audio and video call infrastructure: a meeting API for embedding calls in your own application and a managed workspace for teams, deployable in your own cloud or private environment."
      }
    ]
  },
  {
    "@context": "https://schema.org",
    "@type": "WebSite",
    "@id": "https://cloudguys.io/#website",
    "url": "https://cloudguys.io/",
    "name": "CloudGuys",
    "description": "Self-hosted software, embedded engineers and cloud security advisory for teams that run their own platform.",
    "publisher": {
      "@id": "https://cloudguys.io/#organization"
    },
    "inLanguage": "en"
  },
  {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    "@id": "https://cloudguys.io/blog/embed-video-calls-in-your-app#article",
    "headline": "Embed Video Calls in Your App: Sign a Token, Redeem an Invite, Join",
    "description": "Add video calls to your product in three moves: your backend signs a short-lived token, your app redeems an invite, the client joins. Real code and limits.",
    "image": "https://cloudguys.io/_astro/cover.CEv9kM-S.jpg",
    "datePublished": "2026-11-11T00:00:00.000Z",
    "dateModified": "2026-11-11T00:00:00.000Z",
    "author": {
      "@type": "Person",
      "@id": "https://cloudguys.io/authors/sabbir-ahmed#person",
      "name": "Sabbir Ahmed",
      "jobTitle": "Founder",
      "url": "https://cloudguys.io/authors/sabbir-ahmed",
      "worksFor": {
        "@id": "https://cloudguys.io/#organization"
      },
      "sameAs": [
        "https://github.com/by-sabbir"
      ]
    },
    "publisher": {
      "@id": "https://cloudguys.io/#organization"
    },
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": "https://cloudguys.io/blog/embed-video-calls-in-your-app"
    },
    "articleSection": "Real-time Video",
    "keywords": "Video Call API, WebRTC, LiveKit, Tele, Embedded Video",
    "inLanguage": "en",
    "about": {
      "@type": "SoftwareApplication",
      "@id": "https://tele.cloudguys.io/#software",
      "name": "Tele",
      "url": "https://tele.cloudguys.io"
    }
  },
  {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
      {
        "@type": "ListItem",
        "position": 1,
        "name": "Home",
        "item": "https://cloudguys.io/"
      },
      {
        "@type": "ListItem",
        "position": 2,
        "name": "Blog",
        "item": "https://cloudguys.io/blog"
      },
      {
        "@type": "ListItem",
        "position": 3,
        "name": "Embed Video Calls in Your App: Sign a Token, Redeem an Invite, Join",
        "item": "https://cloudguys.io/blog/embed-video-calls-in-your-app"
      }
    ]
  }
]
```
