A RESTful + WebSocket API for creating and managing video meetings. Built for developers, agents, and teams.
x-api-key header (obtain from Dashboard → API Keys)x-admin-token header (returned when meeting is created)Create a new account. Returns the account API key. A welcome email is sent if RESEND_API_KEY is configured.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address (must be unique) |
password | string | Yes | Minimum 8 characters |
accountType | string | No | "personal" (default) or "company" |
companyName | string | If company | Required when accountType is "company" |
curl -X POST /api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"secret123"}'
# Response
{
"message": "Account created",
"email": "you@example.com",
"apiKey": "mk_abc123...",
"accountType": "personal"
}
Log in to your account. Sets a session cookie used by the dashboard.
| Field | Type | Description |
|---|---|---|
email | string | Account email |
password | string | Account password |
Destroy the current session and clear the session cookie.
Get the current authenticated user's profile. Used by the dashboard to verify login state.
{
"id": 42,
"email": "you@example.com",
"accountType": "personal",
"balance": "25.0000",
"company_id": null,
"is_admin": false,
"created_at": "2026-03-01T12:00:00Z"
}
Request a password reset email. Rate-limited to 3 requests per 15 minutes per IP. Silently succeeds even if email not found (prevents user enumeration).
| Field | Type | Description |
|---|---|---|
email | string | The email address to send the reset link to |
Validate a password reset token. Called client-side to check if the token from the email link is still valid (tokens expire after 1 hour).
| Query Param | Description |
|---|---|
token | The reset token from the email link |
Set a new password using a valid reset token.
| Field | Type | Description |
|---|---|---|
token | string | Reset token from the email link |
password | string | New password (min 8 characters) |
Change the current user's password. Requires authentication. Sends a "password changed" security email on success.
| Field | Type | Description |
|---|---|---|
currentPassword | string | Current account password |
newPassword | string | New password (min 8 characters) |
API keys authenticate REST API calls via the x-api-key header. Multiple keys can be active simultaneously. Keys are prefixed mk_.
List all API keys for the current user.
{
"keys": [
{ "id": 1, "key": "mk_abc123...", "label": "Production", "is_active": true, "created_at": "2026-03-01T12:00:00Z" }
]
}
Generate a new API key.
| Field | Type | Description |
|---|---|---|
label | string | Optional label for this key (e.g. "Production") |
Revoke and permanently delete an API key. Any requests using the deleted key will immediately return 401.
Create a new meeting (instant or scheduled). Requires an active API key. Credits are deducted when the meeting ends based on duration and peak participant count.
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | Meeting title (default: "Untitled Meeting") |
scheduledAt | string | No | ISO 8601 UTC date to schedule the meeting for a future time. If omitted, the meeting is immediately active. |
muteOnJoin | boolean | No | Auto-mute new participants (default: false) |
videoOffOnJoin | boolean | No | Auto-disable video for new participants (default: false) |
maxParticipants | number | No | Maximum concurrent participants (default: 50) |
# Instant meeting
curl -X POST /api/meetings \
-H "Content-Type: application/json" \
-H "x-api-key: mk_yourkey" \
-d '{"title": "Team Standup"}'
# Response
{
"meetingId": "abc-defg-hij",
"adminToken": "d4e5f6...",
"joinUrl": "/join/abc-defg-hij",
"title": "Team Standup",
"status": "active",
"settings": { "muteOnJoin": false, "videoOffOnJoin": false, "maxParticipants": 50, "locked": false, "waitingRoom": false }
}
# Scheduled meeting
curl -X POST /api/meetings \
-H "Content-Type: application/json" \
-H "x-api-key: mk_yourkey" \
-d '{"title":"Weekly Sync","scheduledAt":"2026-04-01T14:00:00Z"}'
# Response
{
"meetingId": "xyz-abcd-efg",
"adminToken": "a1b2c3...",
"joinUrl": "/join/xyz-abcd-efg",
"title": "Weekly Sync",
"scheduledAt": "2026-04-01T14:00:00.000Z",
"status": "scheduled",
"settings": { "muteOnJoin": false, "videoOffOnJoin": false, "maxParticipants": 50, "locked": false, "waitingRoom": false }
}
List all currently active and scheduled meetings for the authenticated API key's user.
{
"meetings": [
{ "meetingId": "abc-defg-hij", "title": "Team Standup", "status": "active", "participantCount": 3, "createdAt": 1710000000000 },
{ "meetingId": "xyz-abcd-efg", "title": "Weekly Sync", "status": "scheduled", "scheduledAt": "2026-04-01T14:00:00.000Z", "participantCount": 0 }
]
}
Get details of a specific meeting including current participants and settings.
{
"meetingId": "abc-defg-hij",
"title": "Team Standup",
"status": "active",
"createdAt": 1710000000000,
"participantCount": 2,
"participants": [
{ "participantId": "a1b2c3", "name": "Alice", "isMuted": false, "isVideoOff": false, "isScreenSharing": false, "joinedAt": 1710000001000 }
],
"settings": { "muteOnJoin": false, "videoOffOnJoin": false, "maxParticipants": 50, "locked": false, "waitingRoom": false }
}
End a meeting, disconnect all participants, and trigger billing. Requires x-admin-token.
curl -X DELETE /api/meetings/abc-defg-hij \
-H "x-api-key: mk_yourkey" \
-H "x-admin-token: d4e5f6..."
Guest meetings are created without authentication. They are in-memory only (no billing, no persistence) and rate-limited to prevent abuse.
Create a guest meeting with no API key. Limited to 5 meetings per hour per IP address. This endpoint powers the homepage "Start a meeting" button.
| Field | Type | Description |
|---|---|---|
title | string | Optional meeting title (default: "Quick Meeting") |
curl -X POST /api/meetings/guest \
-H "Content-Type: application/json" \
-d '{"title": "Quick Sync"}'
# Response
{
"meetingId": "qwerty-abc",
"adminToken": "uuid...",
"joinUrl": "/join/qwerty-abc",
"title": "Quick Sync"
}
POST /api/meetings.scheduledAt (ISO 8601 UTC) to POST /api/meetingsscheduled status until the scheduled time arrivesDELETE /api/meetings/:meetingId
List all scheduled (not yet started) meetings for the authenticated API key's user.
curl /api/meetings/scheduled/list -H "x-api-key: mk_yourkey"
# Response
{
"meetings": [
{ "meetingId": "xyz-abcd-efg", "title": "Weekly Sync", "scheduledAt": "2026-04-01T14:00:00.000Z", "status": "scheduled", "createdAt": 1710000000000 }
]
}
All settings and lock/unlock endpoints require the x-admin-token header.
Update meeting settings live. Changes are broadcast to all participants via the meeting:settings-updated Socket.IO event.
| Field | Type | Description |
|---|---|---|
title | string | Rename the meeting |
muteOnJoin | boolean | Auto-mute new joiners |
videoOffOnJoin | boolean | Auto-disable video for new joiners |
maxParticipants | number | Change the participant cap |
locked | boolean | Lock or unlock the meeting |
waitingRoom | boolean | Enable or disable the waiting room |
Lock the meeting — no new participants can join until unlocked.
Unlock the meeting, allowing participants to join again.
Generate a one-use invite link for a new participant.
| Field | Type | Description |
|---|---|---|
name | string | Optional — pre-fills the participant's name on the join page |
{
"joinUrl": "/join/abc-defg-hij?invite=x1y2z3&name=Bob",
"inviteToken": "x1y2z3"
}
All endpoints require both x-api-key and x-admin-token. Actions are applied immediately via Socket.IO to the target participant.
Force-mute a specific participant. The participant receives an admin:mute event and their audio is disabled client-side.
Send an unmute request to a specific participant.
Remove a participant immediately. They receive an admin:kick event with a reason.
| Field | Type | Description |
|---|---|---|
reason | string | Optional message shown to the removed participant |
Mute every participant simultaneously. Broadcasts meeting:all-muted to the room.
Get the meeting usage history for the authenticated user (or their company). Includes duration, peak participants, and credit cost per meeting.
curl /api/meetings/history -b session_cookie
# Response
{
"meetings": [
{
"id": 1,
"meeting_id": "abc-defg-hij",
"title": "Team Standup",
"started_at": "2026-03-19T10:00:00Z",
"ended_at": "2026-03-19T10:30:00Z",
"duration_minutes": "30.00",
"peak_participants": 4,
"cost_usd": "0.0120"
}
]
}
Register HTTPS endpoints to receive real-time notifications about meeting events. Webhooks are sent as POST requests with a JSON body.
List all configured webhook endpoints for the current user.
{
"webhooks": [
{ "id": 1, "url": "https://your-app.com/webhook", "events": ["meeting.ended","participant.left"], "created_at": "2026-03-01T12:00:00Z" }
]
}
Register a new webhook endpoint.
| Field | Type | Description |
|---|---|---|
url | string | HTTPS URL to receive webhook events |
events | string[] | Array of event names to subscribe to (see below) |
| Event | Description |
|---|---|
meeting.ended | Fired when a meeting ends (via DELETE or all participants leave) |
participant.joined | Fired when a participant joins the meeting |
participant.left | Fired when a participant disconnects |
{
"event": "meeting.ended",
"meetingId": "abc-defg-hij",
"timestamp": 1710000000000,
"data": {
"title": "Team Standup",
"durationMinutes": 30,
"peakParticipants": 4,
"costUsd": "0.0120"
}
}
Remove a webhook endpoint. Events will no longer be sent to that URL.
Connect to the Socket.IO server at the base URL. The same server handles both signaling and meeting state.
import { io } from "socket.io-client";
const socket = io("https://onepizza.io");
| Event | Payload | Description |
|---|---|---|
join-meeting | { meetingId, name, isAdmin, adminToken } | Join a meeting. Pass isAdmin: true and the adminToken to join as host. |
signal:offer | { to: participantId, offer } | Send a WebRTC offer to a specific participant |
signal:answer | { to: participantId, answer } | Send a WebRTC answer |
signal:ice-candidate | { to: participantId, candidate } | Send an ICE candidate |
media:toggle-audio | { isMuted: boolean } | Broadcast your mute state to all participants |
media:toggle-video | { isVideoOff: boolean } | Broadcast your video state |
media:screen-share | { isScreenSharing: boolean } | Broadcast screen-share state |
raise-hand | { isHandRaised: boolean } | Raise or lower your hand |
react | { emoji: string } | Send a floating emoji reaction (👍 ❤️ 😂 🎉 👏) |
recording:broadcast-started | { hostName: string } | Notify participants that the host has started recording |
chat:message | { text: string } | Send a chat message (max 2000 chars) |
| Event | Payload | Description |
|---|---|---|
joined | { participantId, participants[], settings, title, isAdmin } | Confirmed join — includes all current participants and meeting settings |
participant:joined | { participantId, name, isMuted, isVideoOff, isAdmin } | A new participant joined the room |
participant:left | { participantId, name } | A participant disconnected |
participant:updated | { participantId, isMuted?, isVideoOff?, isScreenSharing?, isHandRaised? } | A participant's media or hand state changed |
signal:offer | { from: participantId, offer } | Incoming WebRTC offer |
signal:answer | { from: participantId, answer } | Incoming WebRTC answer |
signal:ice-candidate | { from: participantId, candidate } | Incoming ICE candidate |
admin:mute | {} | You were force-muted by the host |
admin:unmute | {} | The host sent you an unmute request |
admin:kick | { reason: string } | You were removed from the meeting |
meeting:ended | { reason: string } | The meeting was ended by the host or admin |
meeting:all-muted | {} | All participants were muted |
meeting:settings-updated | { muteOnJoin, videoOffOnJoin, maxParticipants, locked, waitingRoom } | Meeting settings changed |
chat:message | { from: participantId, name, text, timestamp } | A chat message was sent in the room |
react | { participantId, emoji } | An emoji reaction from a participant |
recording:started | { hostName: string } | The host has started recording — display consent notice |
error | { message: string } | Something went wrong (meeting not found, locked, full, or scheduled) |
When waiting room is enabled for a meeting (via settings), participants queue up before being admitted by the host.
PATCH /api/meetings/:meetingId/settings with { "waitingRoom": true }.
| Event | Payload | Description |
|---|---|---|
waiting-room:join | { meetingId, name } | Join the waiting queue (called automatically instead of join-meeting when waiting room is active) |
waiting-room:admit | { meetingId, socketId } | Host only — admit a waiting participant into the meeting |
waiting-room:deny | { meetingId, socketId } | Host only — deny and remove a participant from the queue |
| Event | Payload | Description |
|---|---|---|
waiting-room:waiting | { message } | Sent to the waiting participant — confirms they are in the queue |
waiting-room:admitted | {} | You were admitted — proceed to join-meeting |
waiting-room:denied | { message } | The host declined your entry |
waiting-room:participant-waiting | { socketId, name, count, removed? } | Sent to host(s) — a participant is waiting or was admitted/denied |
The service uses a full mesh topology — each participant connects directly to every other participant via WebRTC peer connections.
1. You emit join-meeting → server responds with "joined" (includes all current participants)
2. For each existing participant, you create an RTCPeerConnection and send them an offer via signal:offer
3. They receive your offer → create an answer → send via signal:answer
4. Both sides exchange ICE candidates via signal:ice-candidate
5. Media (audio/video) flows directly peer-to-peer
Supported media tracks:
- Audio: Microphone (mutable)
- Video: Camera (toggleable, device-selectable)
- Screen: Screen share (replaces camera track temporarily)
- Background: Canvas-based blur filter (processed stream replaces camera track)
Returns the STUN/TURN server list used for WebRTC. The web client calls this automatically on join. If TURN credentials are configured via environment variables, they are included here.
# Default response (STUN only)
{
"iceServers": [
{ "urls": "stun:stun.l.google.com:19302" },
{ "urls": "stun:stun1.l.google.com:19302" }
]
}
# With TURN configured
{
"iceServers": [
{ "urls": "stun:stun.l.google.com:19302" },
{ "urls": "turn:your-turn-server.com:3478", "username": "...", "credential": "..." }
]
}
Company accounts let multiple users share a single credit balance and API keys under one workspace. Any credit deducted by a member comes from the company balance.
Create a company account by setting accountType: "company". An invite code is returned, which members use to join.
curl -X POST /api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"owner@acme.com","password":"secret123","accountType":"company","companyName":"Acme Corp"}'
# Response
{
"message": "Account created",
"email": "owner@acme.com",
"apiKey": "mk_abc123...",
"inviteCode": "aBcDeFgHiJkL",
"accountType": "company"
}
Join an existing company workspace using an invite code. The user's balance merges into the company balance.
| Field | Type | Description |
|---|---|---|
inviteCode | string | The invite code from the company owner |
curl -X POST /api/company/join \
-H "Content-Type: application/json" \
-b session_cookie \
-d '{"inviteCode": "aBcDeFgHiJkL"}'
# Response
{ "message": "Joined Acme Corp", "companyId": 1, "companyName": "Acme Corp" }
List all members of the current user's company. Only available to company account owners.
{
"members": [
{ "id": 1, "email": "owner@acme.com", "role": "owner", "joined_at": "2026-03-01T12:00:00Z" },
{ "id": 2, "email": "dev@acme.com", "role": "member", "joined_at": "2026-03-05T08:00:00Z" }
]
}
Remove a member from the company. Only the company owner can do this.
Credits are consumed at meeting end based on duration × peak participants × the rate configured in the admin panel (default: $0.004/participant-minute). If a user belongs to a company, the company balance is used instead of the personal balance. A low-balance alert email is sent when balance drops below $2.00.
Create a Stripe Checkout session to top up credits via credit/debit card. Redirects the user to Stripe's hosted checkout page.
| Field | Type | Description |
|---|---|---|
amountUsd | number | Amount in USD to top up (min $5, max $1000) |
curl -X POST /api/billing/stripe/checkout \
-H "Content-Type: application/json" \
-b session_cookie \
-d '{"amountUsd": 20}'
# Response
{ "url": "https://checkout.stripe.com/c/pay/..." }
Get the USDC (ERC-20) deposit address for your account. Each user/company gets a unique HD wallet address derived from the server's mnemonic. Send USDC on Ethereum mainnet to this address — the server polls for deposits and credits your balance automatically.
curl /api/billing/usdc/address -b session_cookie
# Response
{ "address": "0xABC123..." }
Get balance and transaction history for the current user or their company.
{
"balance": "25.0000",
"transactions": [
{ "id": 1, "amount_usd": "20.0000", "type": "stripe_topup", "description": "Stripe top-up $20", "created_at": "2026-03-10T12:00:00Z" },
{ "id": 2, "amount_usd": "-0.0120", "type": "meeting_charge", "description": "Meeting abc-defg-hij (30m, 4 participants)", "created_at": "2026-03-19T10:30:00Z" }
]
}
Stripe webhook receiver (internal use). Credits are added automatically when Stripe confirms a payment. Configured via STRIPE_WEBHOOK_SECRET.
All errors return JSON: { "error": "<message>" }
| Status | Cause |
|---|---|
| 400 | Validation error (missing field, bad format, e.g. scheduledAt must be in the future) |
| 401 | Missing or invalid x-api-key, or not logged in |
| 402 | Insufficient credits to start a meeting |
| 403 | Missing or invalid x-admin-token |
| 404 | Meeting, participant, or resource not found |
| 409 | Conflict (email already in use, already in a company, etc.) |
| 429 | Rate limit exceeded — slow down requests |
| 500 | Internal server error |
| Endpoint Group | Limit |
|---|---|
| Login / Register | 10 requests / 15 min per IP |
| Forgot / Reset password | 3 requests / 15 min per IP |
| Guest meeting creation | 5 requests / 1 hour per IP |
| API (authenticated) | 100 requests / 15 min per IP |
| Variable | Required | Description |
|---|---|---|
DATABASE_PUBLIC_URL | Yes | PostgreSQL connection string |
SESSION_SECRET | Yes | Session signing secret (min 32 chars, random) |
ADMIN_EMAIL | Yes | Admin account email (seeded on first start) |
ADMIN_PASSWORD | Yes | Admin account password |
APP_URL | No | Public base URL for email links (default: http://localhost:3000) |
PORT | No | HTTP server port (default: 3000) |
NODE_ENV | No | development or production |
ALLOWED_ORIGINS | No | Comma-separated CORS origins (default: http://localhost:3000) |
RESEND_API_KEY | No | Resend API key for transactional emails (silently skipped if unset) |
STRIPE_SECRET_KEY | No | Stripe secret key — disables card payments if unset |
STRIPE_WEBHOOK_SECRET | No | Stripe webhook signing secret |
STRIPE_PRICE_ID | No | Stripe Price ID for credit top-ups |
TURN_URLS | No | TURN server URL (e.g. turn:your-server.com:3478) |
TURN_USERNAME | No | TURN server username |
TURN_CREDENTIAL | No | TURN server password |
CRYPTO_MNEMONIC | No | BIP-39 mnemonic for HD wallet (USDC deposits) — 12-word phrase |
Server-side analytics endpoints for platform administrators. All endpoints require an active admin session (cookie-based).
Feature usage breakdown for tracked events (screen share, recording, chat, reactions, etc.).
| Query Param | Type | Default | Description |
|---|---|---|---|
days | number | 30 | Lookback window in days |
# Response
{
"features": [
{ "feature": "screen_share", "total_uses": 142, "unique_users": 38, "active_days": 22 },
{ "feature": "recording", "total_uses": 67, "unique_users": 15, "active_days": 18 }
],
"days": 30
}
Aggregated error log grouped by event type, route, and message.
| Query Param | Type | Default | Description |
|---|---|---|---|
days | number | 30 | Lookback window in days |
# Response
{
"errors": [
{ "event_type": "error.webrtc", "route": "/join", "message": "ICE connection failed", "count": 12, "last_seen": "2026-03-20T14:30:00Z" }
],
"days": 30
}
Live meeting state from server memory (no DB query). Shows all currently active meetings and aggregate counts.
# Response
{
"activeMeetings": 3,
"totalParticipants": 14,
"activeRecordings": 1,
"activeScreenShares": 2,
"meetingList": [
{ "id": "abc-defg-hij", "title": "Team Standup", "participantCount": 5, "duration": 1200, "isRecording": true }
]
}
Week-over-week user retention for the last 12 weeks.
# Response
{
"retention": [
{ "week": "2026-03-10", "users": 120, "retained": 84 },
{ "week": "2026-03-03", "users": 115, "retained": 79 }
]
}
Server health metrics including memory usage, DB pool stats, and active meeting counts.
# Response
{
"uptimeSeconds": 86400,
"memoryMB": 128.5,
"heapUsedMB": 95.2,
"heapTotalMB": 140.0,
"dbPoolTotal": 20,
"dbPoolIdle": 15,
"dbPoolWaiting": 0,
"activeMeetings": 3,
"scheduledMeetings": 1
}
Meeting distribution by day-of-week and hour. Useful for capacity planning.
| Query Param | Type | Default | Description |
|---|---|---|---|
days | number | 90 | Lookback window in days |
# Response
{
"peakHours": [
{ "dow": 1, "hour": 10, "count": 45 },
{ "dow": 3, "hour": 14, "count": 38 }
],
"days": 90
}
The analytics system tracks the following event types. These are stored in the analytics_events table and queried by the endpoints above.
| Event Type | Trigger | Meta Fields |
|---|---|---|
feature.screen_share | Screen share start/stop | { action: 'start'/'stop' } |
feature.recording | Recording start/stop | { action: 'start'/'stop' } |
feature.chat | Chat message sent | { length } |
feature.chat_reaction | Chat message reaction | { emoji } |
feature.reaction | Floating emoji reaction | { emoji } |
feature.hand_raise | Hand raise toggled | { raised: true/false } |
feature.captions | Captions used | {} |
feature.waiting_room | Waiting room admit/deny | { action: 'admit'/'deny' } |
meeting.participant_joined | Participant joins meeting | { meetingId, participantCount } |
meeting.created | Meeting created | { scheduled, muteOnJoin, waitingRoom } |
meeting.ended | Meeting ended | { durationMinutes, peakParticipants, costUsd } |
curl -X POST https://onepizza.io/api/meetings/guest \
-H "Content-Type: application/json" \
-d '{"title": "Quick Sync"}'
# Open the joinUrl in a browser to join
curl -X POST https://onepizza.io/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"secret123"}'
# Save the apiKey from the response
API_KEY="mk_yourkey"
BASE="https://onepizza.io"
# Create
RESP=$(curl -s -X POST $BASE/api/meetings \
-H "Content-Type: application/json" \
-H "x-api-key: $API_KEY" \
-d '{"title":"Team Standup"}')
MEETING_ID=$(echo $RESP | jq -r '.meetingId')
ADMIN_TOKEN=$(echo $RESP | jq -r '.adminToken')
echo "Join: $BASE/join/$MEETING_ID"
# Check participants
curl -s $BASE/api/meetings/$MEETING_ID -H "x-api-key: $API_KEY" | jq
# Mute a participant
curl -s -X POST $BASE/api/meetings/$MEETING_ID/participants/PARTICIPANT_ID/mute \
-H "x-api-key: $API_KEY" \
-H "x-admin-token: $ADMIN_TOKEN"
# End the meeting
curl -s -X DELETE $BASE/api/meetings/$MEETING_ID \
-H "x-api-key: $API_KEY" \
-H "x-admin-token: $ADMIN_TOKEN"
curl -X POST https://onepizza.io/api/meetings \
-H "Content-Type: application/json" \
-H "x-api-key: $API_KEY" \
-d '{"title":"Daily Standup","scheduledAt":"2026-04-01T09:00:00Z"}'
import { io } from "socket.io-client";
const socket = io("https://onepizza.io");
socket.emit("join-meeting", {
meetingId: "abc-defg-hij",
name: "Bot",
isAdmin: false,
adminToken: null,
});
socket.on("joined", ({ participantId, participants }) => {
console.log("I am", participantId, "— others:", participants.map(p => p.name));
});
socket.on("participant:joined", p => console.log(p.name, "joined"));
socket.on("chat:message", ({ name, text }) => console.log(`${name}: ${text}`));
// Send a chat message
socket.emit("chat:message", { text: "Hello from the bot!" });
// Raise hand
socket.emit("raise-hand", { isHandRaised: true });
// Emoji reaction
socket.emit("react", { emoji: "👍" });
| Category | Feature | Description |
|---|---|---|
| Video | WebRTC video calls | Full mesh peer-to-peer video; camera device selection |
| Video | Screen sharing | Share entire screen or application window |
| Video | Background blur | Canvas-based blur filter on your camera feed |
| Video | Spotlight / pin | Pin any participant to the main view; others go to a filmstrip |
| Audio | Microphone control | Mute/unmute with device selection |
| Audio | Speaking indicator | Blue highlight on the currently speaking tile |
| Collaboration | In-meeting chat | Real-time text chat visible to all participants |
| Collaboration | Emoji reactions | Floating emoji reactions (👍 ❤️ 😂 🎉 👏) visible to everyone |
| Collaboration | Raise hand | Signal to the host you want to speak; badge shown on your tile |
| Collaboration | Recording consent | Host can broadcast recording notice; all participants are notified |
| Host controls | Participant management | Mute, unmute, kick individual participants |
| Host controls | Mute all | Silence every participant with one action |
| Host controls | Meeting lock | Prevent new participants from joining |
| Host controls | Waiting room | Queue participants; host admits or denies each one |
| Scheduling | Scheduled meetings | Plan meetings for a future time; auto-activate at start time |
| API | REST API | Full programmatic meeting management via API key auth |
| API | Socket.IO events | Real-time state sync for custom clients and bots |
| API | Webhooks | Push notifications to your server for meeting and participant events |
| API | Meeting history | Per-meeting usage log with duration, participants, and cost |
| Accounts | Password reset | Email-based reset flow with 1-hour expiry |
| Accounts | Company workspaces | Multi-user accounts with shared credits and invite codes |
| Billing | Usage-based credits | Charged at meeting end: duration × peak participants × rate/min |
| Billing | Stripe payments | Top up credits via credit/debit card (Stripe Checkout) |
| Billing | USDC crypto | Top up credits with USDC (ERC-20) via dedicated HD wallet address |
| Infrastructure | STUN/TURN support | Configurable TURN credentials for NAT traversal |
| Infrastructure | Rate limiting | Per-IP limits on auth, guest meetings, and API endpoints |
| Infrastructure | Compression | gzip/brotli response compression |
| Infrastructure | Security headers | helmet.js CSP, HSTS, and other security headers |