Overview
A REST API for creating video meeting rooms, issuing API keys, and scheduling meetings on Google Calendar. Backed by a Mediasoup SFU.
Base URL
https://your-domain.com/api/v1
Content Type
All request bodies are JSON. Set Content-Type: application/json.
Versioning
The API is versioned in the URL path. The current and only version is
v1.
Current version
https://your-domain.com/api/v1/...
Legacy path
For backwards compatibility, requests to /api/... (without a
version segment) are forwarded to /api/v1/.... New
integrations should always use the versioned path.
# Both of these reach the same handler
https://your-domain.com/api/v1/rooms
https://your-domain.com/api/rooms
| Version | Status | Notes |
|---|---|---|
v1 |
Current | Default. All endpoints below use this version. |
/api/v1 explicitly. That way if a
v2 is ever released, your code keeps working unchanged.
What "breaking change" means here
Any of the following would ship as a new version:
- Renaming or removing a response field
- Changing a field's type (e.g.
roomIdfrom string to number) - Changing the meaning of an existing HTTP status code
- Requiring a new parameter on an existing endpoint
Adding a new optional field, a new endpoint, or a new enum value is
not a breaking change and ships within v1.
Authentication
Two ways to authenticate. Pick one per request.
1. Licence Key recommended for APIs
Pass your key as a Bearer token or an API-Key header. Both work identically.
Bearer token
Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX
API-Key header
X-API-Key: VM-XXXX-XXXX-XXXX-XXXX
2. Session Cookie
When a user logs in through the browser, a signed JWT is set in the
sid cookie. Requests made from the same origin include it
automatically. Useful for the dashboard and any first-party frontend.
| Scope | Session cookie | Licence key |
|---|---|---|
| Create rooms | ✅ | ✅ |
| List own rooms | ✅ (by email) | ✅ (by licence) |
| Create licences | ✅ | ✅ |
| Schedule meetings | ✅ (needs Calendar connected) | ✅ (owner must have connected) |
| Calendar operations | ✅ | ✅ (owner's calendar) |
Errors
All errors return JSON of the form:
{
"ok": false,
"error": "Human-readable message",
"code": "OPTIONAL_MACHINE_CODE"
}
| Status | Meaning |
|---|---|
400 | Bad request — missing or invalid fields |
401 | No valid session or licence key |
404 | Resource not found (or not visible to caller) |
409 | Conflict — e.g. room code was ended and cannot be reused |
429 | Rate limit exceeded |
500 | Server error |
Create Room
Creates a bare meeting room with a server-generated code. No calendar event.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
meetingTitle | string | optional | Human-readable title. Max 200 chars. |
The server generates the room code itself — the client cannot supply one.
Codes use an unambiguous alphabet (no I, O,
0, 1) and are checked against live, known, and
ended rooms before being assigned.
Example
curl -X POST https://your-domain.com/api/v1/rooms \
-H "Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX" \
-H "Content-Type: application/json" \
-d '{"meetingTitle":"Monday Standup"}'
Response 201 Created
{
"ok": true,
"roomId": "42",
"roomName": "K7P2QM",
"meetingTitle": "Monday Standup",
"live": true,
"participantCount": 0,
"createdAt": 1736942400000,
"joinUrl": "https://your-domain.com/dashboard?room=K7P2QM",
"meetingLink": "https://your-domain.com/dashboard?room=K7P2QM",
"created": true,
"createdBy": { "licenceId": "2", "label": "Design team" }
}
roomId is stable across server restarts (it's the DB primary key).
roomName is the human-readable code. joinUrl is what
you send to attendees.
Get Room
Look up whether a room exists and whether it's live.
Example
curl https://your-domain.com/api/v1/rooms/K7P2QM \
-H "Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX"
Response — live
{
"ok": true,
"exists": true,
"roomId": "42",
"roomName": "K7P2QM",
"meetingTitle": "Monday Standup",
"live": true,
"participantCount": 3,
"joinUrl": "https://your-domain.com/dashboard?room=K7P2QM"
}
Response — ended
{ "ok": true, "exists": false, "reason": "ended" }
Response — unknown
{ "ok": true, "exists": false, "reason": "unknown" }
List Licences
Returns every licence owned by the caller. Never includes the plaintext key.
curl https://your-domain.com/api/v1/licences \
-H "Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX"
{
"ok": true,
"licences": [
{
"id": "1",
"keyPrefix": "VM-2YP3",
"keyLast4": "RG6B",
"keyMasked": "VM-2YP3-••••-••••-RG6B",
"label": "Design team",
"plan": "pro",
"seats": 5,
"expiresAt": 1798761600000,
"createdAt": 1736942400000,
"revokedAt": null,
"lastUsedAt": null,
"status": "active"
}
]
}
Create Licence
Request body
| Field | Type | Required | Description |
|---|---|---|---|
label | string | optional | Human-readable label (max 200) |
plan | string | optional | standard, pro, business, enterprise |
seats | number | optional | 1–10000. Default 1. |
expiresAt | ISO date | optional | Omit for no expiry |
curl -X POST https://your-domain.com/api/v1/licences \
-H "Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX" \
-H "Content-Type: application/json" \
-d '{
"label": "Design team",
"plan": "pro",
"seats": 5,
"expiresAt": "2026-12-31"
}'
Response 201 Created
{
"ok": true,
"licence": {
"id": "2",
"keyPrefix": "VM-HASV",
"keyLast4": "HWKS",
"keyMasked": "VM-HASV-••••-••••-HWKS",
"label": "Design team",
"plan": "pro",
"seats": 5,
"expiresAt": 1798761600000,
"createdAt": 1736942400000,
"status": "active"
},
"plaintextKey": "VM-HASV-663P-BZHL-HWKS"
}
Revoke Licence
Marks the licence revoked. Subsequent requests with this key return 401.
curl -X DELETE https://your-domain.com/api/v1/licences/2 \
-H "Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX"
{ "ok": true }
Create Meeting
One-shot endpoint. Creates an SFU room, creates a Google Calendar event on the caller's calendar, and injects the join URL into the event description.
CALENDAR_NOT_CONNECTED.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
title | string | required | Meeting title |
description | string | optional | Free-form description |
startISO | ISO 8601 | required | Start time, e.g. 2026-09-21T09:00:00.000Z |
endISO | ISO 8601 | required | End time (must be after start) |
timeZone | string | required | IANA zone, e.g. America/New_York |
attendees | string[] | optional | Email addresses |
location | string | optional | Physical location |
reminders | object | optional | Google Calendar reminders format |
recurrence | string[] | optional | RRULE strings |
sendUpdates | string | optional | all, externalOnly, none (default none) |
Example
curl -X POST https://your-domain.com/api/v1/schedule \
-H "Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX" \
-H "Content-Type: application/json" \
-d '{
"title": "Monday Standup",
"description": "Weekly sync",
"startISO": "2026-09-21T09:00:00.000Z",
"endISO": "2026-09-21T09:30:00.000Z",
"timeZone": "America/New_York",
"attendees": ["alice@example.com", "bob@example.com"],
"sendUpdates": "all"
}'
Response 201 Created
{
"ok": true,
"roomId": "42",
"roomName": "K7P2QM",
"joinUrl": "https://your-domain.com/dashboard?room=K7P2QM",
"meetingLink": "https://your-domain.com/dashboard?room=K7P2QM",
"roomCreated": true,
"event": {
"id": "k07ae27spfptg4hqln1t7uh5b4",
"summary": "Monday Standup",
"htmlLink": "https://www.google.com/calendar/event?eid=...",
"start": { "dateTime": "2026-09-21T11:00:00+02:00", "timeZone": "America/New_York" },
"end": { "dateTime": "2026-09-21T11:30:00+02:00", "timeZone": "America/New_York" },
"attendees": [
{ "email": "alice@example.com", "responseStatus": "needsAction" },
{ "email": "bob@example.com", "responseStatus": "needsAction" }
]
},
"scheduledBy": { "licenceId": "2", "ownerEmail": "owner@example.com" }
}
Weekly syncJoin here: https://your-domain.com/dashboard?room=K7P2QMRoom code: K7P2QM
List Meetings
Returns all scheduled meetings visible to the caller.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
includeEnded | boolean | false | Include ended meetings (1 or true) |
limit | number | 100 | Max results (capped at 500) |
curl "https://your-domain.com/api/v1/schedule?limit=20" \
-H "Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX"
{
"ok": true,
"count": 2,
"meetings": [
{
"roomId": "2",
"roomName": "EVLND2",
"meetingTitle": "Monday Standup",
"joinUrl": "https://your-domain.com/dashboard?room=EVLND2",
"createdAt": 1789506923070,
"endedAt": null,
"event": {
"id": "2",
"title": "Monday Standup",
"description": "Weekly sync",
"startAt": 1789981200000,
"endAt": 1789983000000,
"timeZone": "America/New_York",
"attendees": ["alice@example.com", "bob@example.com"],
"gcalEventId": "o5hm29o5bnue3gff1ubnihiq9c",
"gcalLink": "https://www.google.com/calendar/event?eid=..."
}
}
]
}
Get Meeting
curl https://your-domain.com/api/v1/schedule/2 \
-H "Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX"
{
"ok": true,
"meeting": {
"roomId": "2",
"roomName": "EVLND2",
"meetingTitle": "Monday Standup",
"joinUrl": "https://your-domain.com/dashboard?room=EVLND2",
"createdAt": 1789506923070,
"endedAt": null,
"event": { "..." : "..." }
}
}
End Meeting
Ends a scheduled meeting. Three things happen:
- Room is soft-deleted in the DB — its code cannot be reused.
- Any live SFU room with that code is force-closed (peers disconnected).
- Best-effort: the Google Calendar event is deleted using the owner's tokens.
curl -X DELETE https://your-domain.com/api/v1/schedule/2 \
-H "Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX"
{
"ok": true,
"ended": true,
"roomId": "2",
"roomName": "EVLND2",
"endedAt": 1789510000000,
"calendarDeleted": true,
"calendarError": null
}
If the meeting is already ended, the response is idempotent:
{ "ok": true, "alreadyEnded": true, "roomId": "2" }
?includeEnded=1 on the
list endpoint to see ended meetings.
Calendar Events
Direct access to the caller's primary Google Calendar.
Query parameters
| Param | Type | Description |
|---|---|---|
from | ISO 8601 | Start of range (defaults to now) |
to | ISO 8601 | End of range |
max | number | 1–250 (default 50) |
q | string | Free-text search |
curl "https://your-domain.com/api/v1/calendar/events?from=2026-09-20T00:00:00Z&max=10" \
-H "Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX"
{
"success": true,
"events": [
{
"id": "abc123",
"summary": "Monday Standup",
"description": "Weekly sync",
"location": "",
"start": "2026-09-21T11:00:00+02:00",
"end": "2026-09-21T11:30:00+02:00",
"allDay": false,
"htmlLink": "https://www.google.com/calendar/event?eid=...",
"hangoutLink": null,
"attendees": [
{ "email": "alice@example.com", "responseStatus": "needsAction" }
],
"organizer": { "email": "owner@example.com", "displayName": "Owner" },
"status": "confirmed"
}
]
}
Create Calendar Event
| Field | Type | Required | Description |
|---|---|---|---|
title | string | required | Event title |
startISO + endISO | ISO 8601 | timed events | Both, with timeZone |
startDate + endDate | YYYY-MM-DD | all-day events | Both; end exclusive |
timeZone | string | for timed | IANA zone |
description, location | string | optional | |
attendees | string[] | optional | Email addresses |
conference | boolean | optional | Create a Google Meet |
curl -X POST https://your-domain.com/api/v1/calendar/create-event \
-H "Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX" \
-H "Content-Type: application/json" \
-d '{
"title": "Design review",
"startISO": "2026-09-22T14:00:00.000Z",
"endISO": "2026-09-22T15:00:00.000Z",
"timeZone": "UTC"
}'
Update Calendar Event
Partial update. Send only the fields you want to change.
curl -X PATCH https://your-domain.com/api/v1/calendar/events/abc123 \
-H "Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX" \
-H "Content-Type: application/json" \
-d '{"title": "Design review (moved)"}'
Delete Calendar Event
curl -X DELETE https://your-domain.com/api/v1/calendar/events/abc123 \
-H "Authorization: Bearer VM-XXXX-XXXX-XXXX-XXXX"
Full Flow Example
Issue a key, schedule a meeting, list, and end it.
1. Create a licence key
# Using your browser session cookie (from DevTools)
curl -X POST https://your-domain.com/api/v1/licences \
-H "Cookie: sid=eyJhbGci..." \
-H "Content-Type: application/json" \
-d '{"label":"Bot","plan":"pro"}'
# Save the plaintextKey from the response
export VM_KEY="VM-ABCD-EFGH-JKLM-NPQR"
2. Schedule a meeting with the key
curl -X POST https://your-domain.com/api/v1/schedule \
-H "Authorization: Bearer $VM_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Quarterly Review",
"startISO": "2026-10-01T15:00:00.000Z",
"endISO": "2026-10-01T16:00:00.000Z",
"timeZone": "America/New_York",
"attendees": ["team@example.com"],
"sendUpdates": "all"
}'
3. List meetings
curl https://your-domain.com/api/v1/schedule \
-H "Authorization: Bearer $VM_KEY"
4. End the meeting
curl -X DELETE https://your-domain.com/api/v1/schedule/42 \
-H "Authorization: Bearer $VM_KEY"
curl Cheatsheet
| Action | Command |
|---|---|
| Create room | curl -X POST .../api/v1/rooms -H "Authorization: Bearer $VM_KEY" -d '{"meetingTitle":"X"}' |
| List meetings | curl .../api/v1/schedule -H "Authorization: Bearer $VM_KEY" |
| Include ended | curl ".../api/v1/schedule?includeEnded=1" -H "Authorization: Bearer $VM_KEY" |
| End meeting | curl -X DELETE .../api/v1/schedule/42 -H "Authorization: Bearer $VM_KEY" |
| List licences | curl .../api/v1/licences -H "Cookie: sid=..." |
| Revoke licence | curl -X DELETE .../api/v1/licences/2 -H "Cookie: sid=..." |
export VM_KEY="VM-XXXX-XXXX-XXXX-XXXX"
— then reference $VM_KEY everywhere.