A RESTful + WebSocket API for creating and managing video meetings. Built for developers, agents, and teams.
/openapi.yaml into Postman / Insomnia / your favourite client generator.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_. Full secrets are shown only once when created; list responses return a non-secret preview.
List all API keys for the current user. The full secret is not returned.
{
"keys": [
{ "id": 1, "key": null, "keyPreview": "mk_abcd1…9xyz", "label": "Production", "is_active": true, "created_at": "2026-03-01T12:00:00Z" }
]
}
Generate a new API key. The full key value is returned only in this response.
| 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 |
recording:broadcast-stopped | {} | Notify participants that the host has stopped recording |
chat:message | { text: string, replyTo?: string } | Send a chat message (max 2000 chars). Optional replyTo is the message ID to reply to. |
chat:react | { msgId: string, emoji: string } | React to a chat message with an emoji |
captions:update | { pid: participantId, text: string, final: boolean } | Send live caption text from speech recognition (interim or final result) |
| Event | Payload | Description |
|---|---|---|
joined | { participantId, participants[], settings, title, isAdmin, muteOnJoin, videoOffOnJoin, isRecording, recordingHostName } | Confirmed join — includes all current participants, meeting settings, and active recording state |
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, replyTo: string|null } | A chat message was sent in the room. replyTo is null or the ID of the message being replied to. |
react | { participantId, emoji } | An emoji reaction from a participant |
recording:started | { hostName: string } | The host has started recording — display consent notice |
recording:stopped | {} | Recording has ended — dismiss consent notice |
chat:react | { msgId: string, participantId, emoji: string } | A participant reacted to a chat message |
captions:update | { pid: participantId, text: string, final: boolean } | Live caption text from a participant's speech recognition |
error | { message: string } | Something went wrong (meeting not found, locked, full, or scheduled) |
**bold**, *italic*, `code`, ```code blocks```.
| Event | Payload | Description |
|---|---|---|
breakout:create | { rooms: number, mode?: "auto"|"manual", assignments?: { [participantId]: roomIndex }, timerMinutes?: number } | Admin starts a breakout session. Room count is clamped to 2–10 and timers to 1–120 minutes. |
breakout:signal:offer | { to, offer, roomIndex } | WebRTC offer inside a breakout room |
breakout:signal:answer | { to, answer, roomIndex } | WebRTC answer inside a breakout room |
breakout:signal:ice-candidate | { to, candidate, roomIndex } | ICE candidate inside a breakout room |
breakout:chat | { text } | Send chat to the participant's assigned breakout room |
breakout:broadcast | { message } | Admin sends message to all breakout rooms |
breakout:end | — | Admin ends all breakout rooms and returns everyone to the main room |
| Event | Payload | Description |
|---|---|---|
breakout:started | { rooms: [{ index, name, participants }], assignments: { [participantId]: roomIndex }, endsAt } | Breakout session started |
breakout:warning | { secondsLeft: 30 } | Timer warning before automatic end |
breakout:signal:offer | { from, offer, roomIndex } | Breakout WebRTC offer |
breakout:signal:answer | { from, answer, roomIndex } | Breakout WebRTC answer |
breakout:signal:ice-candidate | { from, candidate, roomIndex } | Breakout ICE candidate |
breakout:chat | { from, name, text, timestamp, roomIndex } | Room chat message |
breakout:broadcast:received | { text, timestamp } | Host broadcast message |
breakout:ended | { participants: participantId[] } | Breakout session ended and participants returned to the main room |
| Event | Payload | Description |
|---|---|---|
stream:start | { url } | Admin starts broadcast mode and stores the external RTMP URL |
stream:stop | — | Admin stops broadcast mode |
| Event | Payload | Description |
|---|---|---|
stream:started | { hostName } | Broadcast mode started |
stream:stopped | — | Broadcast mode ended |
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 full mesh WebRTC when LiveKit is not configured. When LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET are present, the bundled meeting UI auto-joins a LiveKit SFU room for the main media path while Socket.IO continues to handle meeting state.
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)
MESH_MAX_PARTICIPANTS; deployments with the bundled SFU client path enabled use SFU_MAX_PARTICIPANTS.
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 |
ALLOWED_FRAME_ANCESTORS | No | Comma-separated origins allowed to embed the meeting (CSP frame-ancestors) |
BILLING_RECONCILE_AFTER_HOURS | No | Delay before retrying failed meeting charges |
REDIS_URL | No | Enables the Socket.IO Redis adapter for multi-instance horizontal scaling |
MESH_MAX_PARTICIPANTS | No | Cap on full-mesh WebRTC participants |
LIVEKIT_URL / LIVEKIT_API_KEY / LIVEKIT_API_SECRET | No | Enable LiveKit SFU media, token minting, and room-composite egress |
ENABLE_SFU_CLIENT | No | Set to false to keep LiveKit token minting available but disable the bundled SFU client path |
SFU_PARTICIPANT_THRESHOLD | No | Advisory threshold returned by the SFU config endpoint |
SFU_MAX_PARTICIPANTS | No | Room cap used when the bundled SFU client path is enabled |
STORAGE_BACKEND | No | local (default) or s3 for recordings/files |
S3_BUCKET / S3_REGION / S3_ENDPOINT / S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY | No | S3-compatible storage config (also S3_FORCE_PATH_STYLE, S3_KEY_PREFIX) |
OTEL_EXPORTER_OTLP_ENDPOINT | No | Enables OpenTelemetry tracing (also OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_HEADERS) |
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 } |
Chat messages are persisted to the database during meetings. You can retrieve them as JSON or download as a text file after the meeting.
Get the full chat transcript for a meeting as JSON.
curl https://onepizza.io/api/meetings/abc-defg-hij/transcript \
-H "x-api-key: YOUR_KEY"
# Response:
{
"messages": [
{ "participant_name": "Alice", "text": "Hello everyone!", "created_at": "2026-03-22T14:30:00Z" },
{ "participant_name": "Bob", "text": "Hi Alice!", "created_at": "2026-03-22T14:30:05Z" }
]
}
Download the chat transcript as a plain text file. Returns Content-Disposition: attachment.
curl -O https://onepizza.io/api/meetings/abc-defg-hij/transcript/download \
-H "x-api-key: YOUR_KEY"
# Downloaded file format:
# [14:30:00] Alice: Hello everyone!
# [14:30:05] Bob: Hi Alice!
Upload browser-recorded meeting files to the server for storage and later retrieval.
Upload a recording file (multipart/form-data). Max file size: 500 MB. Accepts video/* and audio/* MIME types.
| Field | Type | Required | Description |
|---|---|---|---|
recording | File | REQUIRED | The recording file (video/webm, etc.) |
curl -X POST https://onepizza.io/api/meetings/abc-defg-hij/recordings \
-H "x-api-key: YOUR_KEY" \
-F "recording=@meeting-recording.webm"
# Response (201):
{
"id": 1,
"filename": "meeting-recording.webm",
"size_bytes": 15728640,
"created_at": "2026-03-22T15:00:00Z"
}
List all recordings for a meeting.
curl https://onepizza.io/api/meetings/abc-defg-hij/recordings \
-H "x-api-key: YOUR_KEY"
# Response:
{
"recordings": [
{ "id": 1, "filename": "meeting-recording.webm", "size_bytes": 15728640, "created_at": "2026-03-22T15:00:00Z" }
]
}
Download a recording file by ID. Returns the file with Content-Disposition: attachment.
curl -O https://onepizza.io/api/recordings/1/download \
-H "x-api-key: YOUR_KEY"
In-meeting collaboration features are available over both REST (for hosts and bots) and Socket.IO (for live updates). REST endpoints accept either an API key (x-api-key) or a logged-in session; host-only actions additionally require the x-admin-token.
Create a poll (host). pollType is one of multiple_choice, yes_no, rating.
curl -X POST https://onepizza.io/api/meetings/abc-defg-hij/polls \
-H "x-api-key: YOUR_KEY" -H "x-admin-token: ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"pollType":"multiple_choice","question":"Lunch?","options":["Pizza","Tacos"]}'
Cast a vote. Body: { "vote": 0 } (option index; legacy opt_N ids also accepted).
Close a poll and reveal results (host).
List polls with current tallies.
Submit a question. Body: { "text": "..." }.
Upvote a question (one per participant).
Mark a question answered (host).
Dismiss a question (host).
List questions sorted by upvotes.
Fetch the current collaborative notes for a meeting.
Replace the notes (host). Body: { "content": "..." } (max 10,000 chars).
Upload a file to share in the meeting (multipart/form-data, field file). Stored via the configured storage backend (local disk or S3).
List files shared in the meeting.
Download a shared file by ID.
Per-participant join/leave/duration report (host).
Download the attendance report as a file (host).
Reusable meeting presets and recurring schedules. REST endpoints accept an API key or session.
List your meeting templates (plus seeded defaults).
Create a template. Body: { "name": "string", "title": "string", "settings": {} } — only name is required (session auth).
Update a template (session auth).
Delete a template (session auth).
List recurring meeting schedules. Legacy alias: GET /api/meetings/recurring returns deprecation headers and points to this endpoint.
Create a recurring schedule. Body: { "title": "string", "rrule": "string", "timezone": "string", "settings": {} } — title and rrule are required, timezone defaults to UTC (iCal RRULE). Legacy alias: POST /api/meetings/recurring.
Update a recurring schedule. Legacy alias: PUT /api/meetings/recurring/:id.
Delete a recurring schedule. (Alias: DELETE /api/meetings/recurring/:id.)
Meetings use full-mesh WebRTC by default. LiveKit credentials enable the bundled client to publish and subscribe through LiveKit SFU automatically, with mesh fallback if the SDK, token, or connection path fails. E2EE remains a mesh-only feature because the current insertable-stream implementation is not wired into the SFU path.
Reports whether LiveKit is enabled, its URL, the advisory threshold (SFU_PARTICIPANT_THRESHOLD, default 8), meshParticipantCap, roomParticipantCap, sfuMaxParticipants, and whether the bundled client has SFU media integration.
Mints a short-lived LiveKit access token (4h TTL) for a participant, authenticated by the per-participant sfuAuthToken issued at join.
Starts LiveKit room-composite egress to an rtmp://, rtmps://, or srt:// destination. Requires the meeting admin token.
Accepts periodic client quality samples (media mode, RTT, jitter, loss, bitrate, peer count) authenticated by the live participant token. Hosts can read recent samples with GET /api/meetings/:meetingId/quality.
Health check endpoints for monitoring and orchestration (Kubernetes, Railway, etc.). No authentication required.
Full health check — verifies database connectivity.
// 200 OK
{ "status": "ok", "uptime": 123.45, "timestamp": "2026-06-30T12:00:00.000Z", "meetings": 0 }
// 503 Service Unavailable
{ "status": "error", "error": "Database unavailable" }
Liveness probe — always returns 200 if the process is running. Use for Kubernetes liveness checks.
// 200 OK
{ "status": "ok" }
Readiness probe — returns 200 only if the database is reachable. Use for Kubernetes readiness checks and load balancer health.
// 200 OK
{ "status": "ok", "db": "connected" }
// 503 Service Unavailable
{ "status": "error", "db": "unavailable" }
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: "👍" });
The meeting UI supports the following keyboard shortcuts. All shortcuts work when focus is not inside a text input or textarea.
| Key | Action |
|---|---|
M | Toggle microphone mute/unmute |
Space | Push-to-talk (hold to temporarily unmute while muted) |
V | Toggle camera on/off |
S | Toggle screen sharing |
H | Raise or lower hand |
R | Toggle recording (host only) |
C | Toggle live captions |
B | Background effects (blur / virtual background) |
P | Picture-in-Picture pop-out |
L | Lock / unlock meeting (host only) |
T | Toggle chat panel |
U | Toggle participants panel |
Q | Toggle polls / Q&A panel |
Esc | Close panels / exit focus view / close modals |
? | Show keyboard shortcuts help dialog |
Space bar while muted to temporarily unmute. Releasing the key re-mutes automatically. This does not interfere with the mute button.
| 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 |
| Collaboration | Live captions | Browser speech recognition broadcast to all participants |
| Collaboration | Polls | Multiple-choice, yes/no, or rating polls with live tallies |
| Collaboration | Q&A | Upvotable question queue; host can answer or dismiss |
| Collaboration | Shared notes | Collaborative notes pad synced to everyone |
| Collaboration | File sharing | Upload and share files in the meeting (local or S3 storage) |
| Collaboration | Breakout rooms | Split participants into isolated sub-rooms with timer + host broadcast |
| Collaboration | Broadcast mode | LiveKit RTMP/SRT egress in SFU rooms; LIVE indicator/manual target fallback in mesh rooms |
| Host controls | Attendance reports | Per-participant join/leave/duration export |
| Scheduling | Meeting templates | Reusable presets for meeting settings |
| Scheduling | Recurring meetings | RRULE-based recurring schedules |
| 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 | LiveKit SFU (optional) | Bundled client media path, per-participant token minting, and room-composite egress |
| Infrastructure | Redis scaling (optional) | Socket.IO Redis adapter for multi-instance horizontal scaling |
| Infrastructure | S3 storage (optional) | S3-compatible object storage for recordings and shared files |
| Infrastructure | OpenTelemetry (optional) | Distributed tracing via OTLP when configured |
| 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 |
onepizza.io ships with a built-in Model Context Protocol server (mcp-server.js) that exposes meeting management as tools any MCP-compatible AI agent can call — Claude Desktop, Claude Code, Cursor, Continue.dev, and more.
stdio — run locally for desktop agents (npm run mcp)HTTP / SSE — run on port 3100 for remote agents (npm run mcp:http)
# .env or shell env
ONEPIZZA_API_URL=https://onepizza.io # API base URL
ONEPIZZA_API_KEY=mk_... # your account API key
{
"mcpServers": {
"onepizza": {
"command": "node",
"args": ["/absolute/path/to/meetingservice/mcp-server.js"],
"env": {
"ONEPIZZA_API_URL": "https://onepizza.io",
"ONEPIZZA_API_KEY": "mk_yourkey"
}
}
}
}
The MCP server exposes these tools. Each is a thin wrapper over the REST or Socket.IO API documented above — agents can chain them naturally to run a full meeting.
| Tool | Purpose | Underlying API |
|---|---|---|
create_meeting | Create an instant or scheduled meeting | POST /api/meetings |
list_meetings | List active & scheduled meetings | GET /api/meetings |
get_meeting | Inspect a meeting (participants, settings) | GET /api/meetings/{id} |
end_meeting | End a live meeting / cancel a scheduled one | DELETE /api/meetings/{id} |
update_meeting_settings | Toggle muteOnJoin, lock, max participants, title | PATCH /api/meetings/{id}/settings |
mute_participant / unmute_participant | Mute / unmute one participant | POST /participants/{id}/{mute|unmute} |
kick_participant | Remove a participant (with reason) | POST /participants/{id}/kick |
mute_all | Mute every participant at once | POST /mute-all |
join_meeting | Connect a bot via Socket.IO; buffers events | Socket.IO |
leave_meeting | Disconnect a joined bot & flush buffered events | Socket.IO |
send_chat_message | Post chat from the bot | Socket.IO chat:message |
send_reaction | Send a 👍 ❤️ 😂 🎉 👏 reaction | Socket.IO react |
create_poll | Create a multiple-choice / yes-no / rating poll (admin bot) | Socket.IO poll:create |
get_poll_results | Read live poll results | GET /api/meetings/{id}/polls |
get_questions | Read Q&A questions sorted by upvotes | GET /api/meetings/{id}/questions |
create_breakout_rooms / end_breakout | Manage breakout rooms (admin bot) | Socket.IO breakout:* |
create_template / list_templates | Reusable meeting setting presets | /api/templates |
create_recurring / list_recurring | RRULE-based recurring meetings | /api/recurring |
get_meeting_notes / update_meeting_notes | Read / write the shared notes pad | /api/meetings/{id}/notes |
get_attendance | Pull the attendance report | /api/meetings/{id}/attendance |
| Resource URI | Description |
|---|---|
meetings://active | Dynamic, real-time list of every active meeting on the server |
An MCP-aware agent will pick this apart into create_meeting with scheduledAt and waitingRoom: true, then later join_meeting + send_chat_message. No custom integration code required.
Because the API ships with a full OpenAPI 3.0 spec (canonical JSON at /openapi.json), you can generate a typed client in any language with one command. Examples:
npx openapi-typescript https://onepizza.io/openapi.yaml -o ./onepizza.d.ts
# or, runtime client:
npx @hey-api/openapi-ts -i https://onepizza.io/openapi.yaml -o ./client
pip install openapi-python-client
openapi-python-client generate --url https://onepizza.io/openapi.yaml
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
oapi-codegen -package onepizza https://onepizza.io/openapi.yaml > onepizza.go
Use File → Import and paste https://onepizza.io/openapi.yaml. Every endpoint, parameter, and example response is included with the correct auth headers prefilled.
The /sdk folder documents the canonical spec, auth schemes, and ready-to-run openapi-generator-cli commands for TypeScript, Python, and Go. Official packages are planned but not yet published — generate your own from the spec for now. See sdk/README.md.