Skip to content
Documentation menu

Guides

Campus calls in apps

Use REST and hosted video tiles to share an existing campus call and invite members into app activities.

Access and login

App calls are available by default. Register calls:read for silent call availability, the approved public call roster, and hosted tiles. Add calls:control for invitations, call/stage actions, playback mute, and activity offers. Control requires read. Existing registrations gain no permissions automatically; adding these scopes requires fresh member consent.

Complete the normal version 2 backend login. Its ready and authenticate replies are unchanged. The server-held bearer token proves login for that launch. Use https://zetapad.com/v1/apps/calls as the production REST base URL and Authorization: Bearer <access_token> on every API request. There is no calls SDK or calls postMessage API to implement.

The launch must be mounted in its owning campus tab. POST /v1/apps/calls/attach requests access to that tab's accepted call. The response is { "grantId": "...", "status": "approval" | "active" }. Zetapad shows Use this call in this app before providing roster or tiles. Approval lasts for that participation while app consent and configuration remain valid. Each member approves independently. A reload starts with native calls, a fresh launch, and fresh backend login.

Use the token only from your backend. All endpoints recheck scopes, app publication, current consent, launch, tab, and participation. Disconnecting or deleting an app, changing permissions, leaving a call, or losing login access revokes presentation. Renew login with a fresh normal authorization code before the 900-second token expiry. Renewal in the same launch preserves the live call.

Developers can edit published apps. A registration save invalidates old launches and credentials; stop polling and clear stale call bindings. Reopen for a fresh launch and backend login. Scope or origin changes require updated member consent and native call approval before new presentation. See scope changes and reauthentication. The native call continues during recovery.

Silent call availability

After normal login and scope consent, call GET /v1/apps/calls/status from your backend with calls:read. This check requires no Use this call in this app approval and shows no prompt. It creates no grant or command, renews no leases, and changes no presentation, devices, or media.

Situation in the owning tabResponse
No accepted, unexpired call participation{ "inCall": false, "canAttach": false, "access": "unavailable" }
Accepted participation without current app approval{ "inCall": true, "canAttach": true, "access": "approvalRequired" }
Accepted participation with remembered current app approval{ "inCall": true, "canAttach": true, "access": "approved" }

inCall means an accepted call with valid participation in this launch's owning campus tab. Pending incoming or outgoing invitations do not count. An accepted call in another tab is not reported. The endpoint accepts no user, call, participation, or tab selector and returns only these three fields: no IDs, roster, identities, media status, or credentials. Invalid login, stale launches, revoked consent, or expired mounts return normal errors rather than an unavailable result.

Use canAttach to offer Use current call in this app. On the member's choice, request /attach; Zetapad then asks for approval if needed. approved describes remembered permission only: it does not create an active grant or hand over presentation. Always attach and prepare before showing tiles. Availability is advisory and can change immediately; /attach independently rechecks current authorization and participation.

With calls:control, an unavailable result can accompany an option to start a call through native controls. A pending invitation or call in another tab may still prevent starting; never switch or leave a call automatically. Recheck status after native acceptance. Keep these requests uncached and check when entering or returning to the call view; avoid a tight polling loop.

Implementation sequence

  1. Register the call scopes and configure hosted tile embedding.
  2. Complete ready, authenticate, and the backend token exchange using the browser and server examples. Retain the exact responding parent origin for tile URLs.
  3. Read /status silently and offer Use current call in this app when canAttach is true. Do not call /attach merely to check availability.
  4. On the member's choice, request /attach. With control access, /start or /accept can open native controls when needed; wait for an applied outcome and accepted participation before attaching. An outgoing invitation alone is unavailable. Never replace another active call automatically.
  5. Store the returned grant in the visitor's server session and poll /grants/{grantId} until native approval returns active. An approval response contains no roster. Stop on denial or authorization failure.
  6. Prepare a presentation, request the current speaker tiles your layout needs, mount their hosted iframes, and request commit. Audio-only preparation requires no tiles.
  7. Drive a heartbeat from the app browser every five seconds, renew normal login before expiry, and release when the visitor leaves the view. Restart preparation after restoration or reload.

The hosted call example includes executable helpers and a mapping from app-owned routes to these endpoints. Your app backend resolves grants, presentations, and command IDs from its authenticated session. It does not accept those bindings or bearer tokens as authorization from browser parameters. Keep state separate for each mounted launch when a visitor has multiple tabs.

Public call state

GET /v1/apps/calls/grants/{grantId} returns approval status. Once active it includes callId, participationId, hostUserId, role, stage state, capacity, the whole public participants roster, and native media status. Poll through your backend for updates.

Each participant contains permanent userId, current participationId, username, avatarUrl, role, presence, and stageState. Presence is connected, reconnecting, or leaving. The call supports 100 members with six speaker seats. Private profile fields remain under their existing scopes. Responses contain no Firebase UID, credential, room token, or track. Local media status has camera, microphone, playback mute, connection, and up to six speaker media flags.

Use userId for saved app data and recipient hints. Use participationId for tiles and stage moderation targetId; it changes when the member rejoins. Treat usernames and avatars as display data. Refresh the roster before preparing a new subset, and tolerate speakers departing between roster and tile requests.

Hosted presentation

  1. POST /v1/apps/calls/grants/{grantId}/presentations with { "mode": "video" } or { "mode": "audioOnly" } returns presentationId and status: "preparing".
  2. For video, POST /v1/apps/calls/presentations/{presentationId}/tiles with { "participationId": "...", "fit": "cover", "label": true } returns tileId and url. Fit can be cover or contain. Only current accepted speakers are eligible, including self-view from already authorized call video. Each presentation permits six unique tiles.
  3. Return those URLs from your backend and mount ordinary iframes. Resolve the relative URL against the actual campus origin that answered the existing ready handshake. The tile and the owning campus window must have the exact same origin, including port. Do not proxy tile documents through your app origin.
  4. POST /v1/apps/calls/presentations/{presentationId}/commit requests the handoff. Its initial response has status: "commitRequested"; Zetapad waits for its trusted tile code to report a rendered video frame or an appropriate placeholder. Every selected tile must be ready and visible. Video mode requires at least one ready tile when the stage has speakers. Native presentation stays usable while preparing.
  5. Send POST /v1/apps/calls/presentations/{presentationId}/heartbeat every five seconds from your backend while its visitor session remains active. The browser can call an ordinary protected route on your own backend to drive this lease; no calls messaging library is needed. An unattended server timer should not continue claiming a failed visitor is active.
  6. POST /v1/apps/calls/presentations/{presentationId}/release returns presentation to Zetapad. Released IDs cannot commit again; prepare a fresh presentation to change the tile subset. A heartbeat response reports the presentation status, including app after trusted handoff and released after release. A restored presentation rejects further heartbeats; clear it from your session and prepare again after valid call access.

The app can resize, position, clip, and round iframe containers, including circular tiles. Hosted cropping and labels remain native. Existing speaker audio stays in Zetapad, even when the app selects two tiles or audio-only mode. No second room, device capture, audio renderer, or media publication is created during handoff.

Tiles use one-use bootstraps, same-origin document access, and exact window binding. They contain no auth provider, LiveKit connection, or capture. They attach existing video directly; tracks are never sent through postMessage. Tile embedding headers allow only the registered app and the tile's Zetapad origin. Your page CSP must permit that campus origin in frame-src. Mount tiles directly inside the registered app iframe, using an ordinary unsandboxed child iframe; do not remove its same-origin capability with an additional sandbox. Do not reuse a tile URL after reload or put it in a different window. Ordinary campus pages remain unembeddable.

One persistent native call panel holds camera/microphone status, playback mute, Leave, and Return call to Zetapad. It stays available during preparation and becomes a compact overlay after app handoff, releasing sidebar space. Tablets and larger screens support native left/right docking; phones float the same controls along the selected edge. Release, exit, reload, authorization loss, invalid tiles, or fifteen seconds of missed foreground health restores native presentation. Video mode also restores if required tiles stay invisible or unready for fifteen seconds, even while their heartbeat continues. The timeout pauses while the campus tab is hidden, with a fresh grace period when shown. Capture and membership continue during presentation changes. Playback mute survives refresh for the same participation and clears on departure.

Trusted readiness proves that the tile surface works; it cannot prove your CSS has left it unobscured. Keep tiles visible and preserve the native return control.

Zetapad renews its native launch lease independently of roster polling, media reports, and call commands. Hidden/shown transitions update the native lease even while those requests are pending. App presentation heartbeats remain your backend's responsibility: keep them separate from gameplay requests, bound their duration, and retry temporary network or server failures while the visitor is active. Forward error statuses and codes so real access loss stops the heartbeat. Hidden-tab recovery does not extend backend login expiry or restore revoked permissions.

Adaptive video quality

Zetapad bounds new camera capture to 1280 × 720 at up to 24 fps, allowing lower resolutions or frame rates when the device cannot provide them. Native calls, private preview, and app calls share that camera track. Handoffs, resizing, and audio-only viewing do not reacquire devices or change outgoing camera/microphone choices.

For a 1280 × 720 source, VP8 simulcast offers approximate 90p, 360p, and 720p layers where the browser supports them. Their maximum bitrates are 60 kbps, 400 kbps, and 1 Mbps, with frame-rate ceilings of 8, 15, and 24 fps respectively. Lower-resolution sources can have fewer layers; the original source uses the 1 Mbps / 24 fps ceiling. Source aspect ratio and browser capabilities determine actual dimensions. These are encoding ceilings, not guaranteed received quality or total upload limits. Dynacast pauses higher layers when nobody needs them.

Hosted tiles report their actual CSS size, visibility, and decoded aspect ratio to Zetapad. Each remote speaker adapts independently at one video pixel per CSS pixel, accounting for cover versus contain. A small tile receives an available smaller layer; 360p is a usual viewing size rather than a fixed limit. Enlarging a tile can request up to the available 720p source. The shared bandwidth budget caps quality and recovers gradually; resizing cannot bypass that ceiling. Hidden tiles stop receiving video. After successful handoff, the native panel detaches and no longer adds video-size demand.

Use the actual visible dimensions for your iframe containers. Avoid oversized surfaces that CSS scales or clips down: arbitrary styling cannot prove how much of a tile the member sees. For a view needing no video, prepare audioOnly. This stops incoming video and retains all authorized speaker audio once in Zetapad. It does not turn off outgoing camera video; other call members may still be watching that speaker.

No new quality scope, resolution REST parameter, or app-side media SDK is required. A larger native or app viewer elsewhere in the call can keep a publisher's higher layer active. Adaptive receiving and dynacast reduce network/encoding work, while the explicit camera defaults bound capture; tiny local tiles alone do not lower the publisher's camera source resolution.

Call requests

Each start, accept, invite, action, playback, or activity invitation request uses a unique clientId of 16–128 URL-safe characters, such as crypto.randomUUID(). Retry the same ID with the same payload; changed payloads fail. The response returns commandId. Poll GET /v1/apps/calls/commands/{commandId} through your backend for pending, working, applied, declined, failed, or expired and optional result. Stop at a terminal outcome. Commands expire after one minute. The app command limit is thirty requests per launch per minute; existing call invitation limits remain five per member per minute.

EndpointBody and native behavior
POST /v1/apps/calls/start{ clientId, recipientId?, role? }; native recipient and role confirmation; an existing call is never switched or left automatically
POST /v1/apps/calls/accept{ clientId }; native confirmation of the current incoming invitation
POST /v1/apps/calls/grants/{grantId}/commands/invite{ clientId, recipientId?, role? }; native invitation controls
POST /v1/apps/calls/grants/{grantId}/commands/action{ clientId, action, targetId? }; native interaction and existing server call rules
POST /v1/apps/calls/grants/{grantId}/commands/playback`{ clientId, muted: true
POST /v1/apps/calls/grants/{grantId}/commands/activityInvite{ clientId, activityId }; native current-member recipient selection

role is speaker or audience; omission retains automatic allocation. An explicit speaker request fails if all six seats are occupied or reserved. Participant 101 is rejected. Online checks, expiration, and retry rules apply.

recipientId is an optional permanent public userId; it preselects a recipient in the native sheet. The member confirms recipients and roles there. /start and /accept require mounted backend login and calls:control but no existing call grant. Grant-bound commands require native call-access approval. A queued or applied invitation does not mean its recipient has accepted; read the roster after acceptance.

Supported actions: cameraOn, cameraOff, unmute, mute, leave, end, stage/request, stage/approve, stage/reject, stage/join, stage/cancel, stage/leave, stage/demote. Camera-on, unmute, joining stage, and call acceptance require native member interaction. Camera-off and mute apply immediately. Only the actual current call host can approve, reject, demote, or end the call. Stage approval reserves a seat; capture starts only after the member joins explicitly.

Shared app activities

Register a backend session using POST /v1/apps/calls/activities with { "grantId": "...", "sessionKey": "server-owned-match-key", "label": "Math matchup" }. The opaque activityId belongs to this app and call. Session keys are 1–256 characters and labels 1–80. Activity URLs and user-supplied keys do not authorize entry.

GET /v1/apps/calls/activities/{activityId} returns the label and call ID only after current per-call approval and matching participation. It requires calls:control and never returns the session key.

Any accepted member with app call control access can offer the activity through commands/activityInvite. Zetapad defaults to the other current members and allows recipient selection. Offers have a separate five-offers-per-member-per-minute limit; recipient invitations expire after five minutes. Recipients may accept or decline independently, and the original call continues.

Acceptance opens the registered app and binds the invitation to the new launch. Complete normal backend login, attach, and obtain native call-access approval. Then call GET /v1/apps/calls/activity-entry from your backend to obtain verified { entryId, activityId, sessionKey, label, userId, expiresIn }. Use that verified userId and server-returned session key to join your own multiplayer session. Never authorize entry from a URL, activity ID, or browser-supplied key. A current entry lease lasts twenty seconds and must be rechecked while protected activity actions continue. Departure, app exit, permission loss, or lease expiry ends access; after expiry the member must accept a current invitation again.

Each member independently decides whether the app takes over their presentation. Your backend owns game state, matchmaking, synchronization, and stored activity data.

Errors and recovery

Failures return { "error": { "code": "...", "message": "..." } } with Cache-Control: private, no-store. Keep your own app-call routes uncached as well.

ResponseApp behavior
401 INVALID_TOKENComplete fresh normal authentication; do not keep polling with expired login
403 ACCESS_REVOKED or APP_UNAVAILABLEStop heartbeat and protected activity actions, clear stale bindings, and let native recovery proceed
403 FORBIDDENCheck scopes, approval, current tab participation, role, or activity invitation; show the returned message
400 BAD_USER_INPUTCorrect the request, speaker selection, or reused clientId payload
409 CALL_CONFLICTRefresh current call state; leave any existing call to the member's explicit decision
429 RATE_LIMITEDBack off; do not automatically replay an invitation under a new ID
503Show a retry state and keep native controls usable

On app reload, run the existing ready/login sequence again, attach, and build a fresh presentation. Do not replay stored tile URLs or commit a released ID. Test actual nested video playback, camera-off placeholders, cover/contain, resize, two-tile subsets, audio-only, Return, app exit, heartbeat loss, and token renewal in Chrome and Firefox before publishing your integration.