Overview

A REST API for creating video meeting rooms, issuing API keys, and scheduling meetings on Google Calendar. Backed by a Mediasoup SFU.

Rooms
Server-generated codes, join URLs, live participant counts.
Licence Keys
Hashed-at-rest API keys. Plaintext shown once at creation.
Scheduling
One-shot endpoint that creates a room and a calendar event.
Dual Auth
Session cookie for browsers, licence key for machines.

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
VersionStatusNotes
v1 Current Default. All endpoints below use this version.
Recommendation Pin your integration to /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:

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
Where to get a key Log in to the dashboard, go to Settings → Licences, and click Generate licence key. The plaintext is shown once — copy it immediately.

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.

ScopeSession cookieLicence 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"
}
StatusMeaning
400Bad request — missing or invalid fields
401No valid session or licence key
404Resource not found (or not visible to caller)
409Conflict — e.g. room code was ended and cannot be reused
429Rate limit exceeded
500Server error

Create Room

Creates a bare meeting room with a server-generated code. No calendar event.

POST /api/v1/rooms

Request body

FieldTypeRequiredDescription
meetingTitlestringoptionalHuman-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.

GET /api/v1/rooms/:roomName

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

GET /api/v1/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

POST /api/v1/licences

Request body

FieldTypeRequiredDescription
labelstringoptionalHuman-readable label (max 200)
planstringoptionalstandard, pro, business, enterprise
seatsnumberoptional1–10000. Default 1.
expiresAtISO dateoptionalOmit 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"
}
Store the plaintext now. The key is hashed before it's stored — the API will never return the plaintext again. If you lose it, revoke and generate a new one.

Revoke Licence

DELETE /api/v1/licences/:id

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.

POST /api/v1/schedule
Calendar access required. For licence-key calls, the licence owner must have connected Google Calendar at least once via the dashboard. Otherwise you'll get CALENDAR_NOT_CONNECTED.

Request body

FieldTypeRequiredDescription
titlestringrequiredMeeting title
descriptionstringoptionalFree-form description
startISOISO 8601requiredStart time, e.g. 2026-09-21T09:00:00.000Z
endISOISO 8601requiredEnd time (must be after start)
timeZonestringrequiredIANA zone, e.g. America/New_York
attendeesstring[]optionalEmail addresses
locationstringoptionalPhysical location
remindersobjectoptionalGoogle Calendar reminders format
recurrencestring[]optionalRRULE strings
sendUpdatesstringoptionalall, 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" }
}
Attendees get the join link. The event description now contains:

Weekly sync
Join here: https://your-domain.com/dashboard?room=K7P2QM
Room code: K7P2QM

List Meetings

GET /api/v1/schedule

Returns all scheduled meetings visible to the caller.

Query parameters

ParamTypeDefaultDescription
includeEndedbooleanfalseInclude ended meetings (1 or true)
limitnumber100Max 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=..."
      }
    }
  ]
}
Scope rules. A licence key sees only meetings created by that licence. A session cookie sees only meetings created by the logged-in user's email.

Get Meeting

GET /api/v1/schedule/:roomId
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

DELETE /api/v1/schedule/:roomId

Ends a scheduled meeting. Three things happen:

  1. Room is soft-deleted in the DB — its code cannot be reused.
  2. Any live SFU room with that code is force-closed (peers disconnected).
  3. 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" }
Soft-delete only. The DB row is retained for audit. Pass ?includeEnded=1 on the list endpoint to see ended meetings.

Calendar Events

Direct access to the caller's primary Google Calendar.

GET /api/v1/calendar/events

Query parameters

ParamTypeDescription
fromISO 8601Start of range (defaults to now)
toISO 8601End of range
maxnumber1–250 (default 50)
qstringFree-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

POST /api/v1/calendar/create-event
FieldTypeRequiredDescription
titlestringrequiredEvent title
startISO + endISOISO 8601timed eventsBoth, with timeZone
startDate + endDateYYYY-MM-DDall-day eventsBoth; end exclusive
timeZonestringfor timedIANA zone
description, locationstringoptional
attendeesstring[]optionalEmail addresses
conferencebooleanoptionalCreate 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

PATCH /api/v1/calendar/events/:eventId

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

DELETE /api/v1/calendar/events/:eventId
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

ActionCommand
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=..."
Tip. Export your key once per shell: export VM_KEY="VM-XXXX-XXXX-XXXX-XXXX" — then reference $VM_KEY everywhere.