Developers
HTTP API
The HTTP API under /api/v1 serves the web client, the mobile apps and the administration. Identity covers sign-in and tokens; the realtime connection is not part of this API. A tenant's own systems use the partner API instead, a Connect service with API keys of its own, and receive its webhooks; the admin operations under /api/v1/admin/api-keys and /api/v1/admin/webhooks manage both (Partner API).
Versions
- Every path starts with
/api/v1, except the OAuth discovery documents under/.well-knownand the web app manifest. - Within a version, changes are additive: new operations, new optional fields, new values the client is told to expect. Clients ignore fields they do not know.
- Removing or changing a field, a status or a meaning needs
/api/v2. Both versions are served until the clients in use have moved.
Errors
Errors are RFC 9457 problems, served as application/problem+json with the status they describe:
{ "type": "about:blank", "title": "Not Found", "status": 404, "detail": "no room at this path" }
detail explains the problem to people in the language the request's Accept-Language asks for, English when the server speaks none of its languages (Languages); so does the page a browser lands on when a sign-in cannot go on, with its lang and dir. A server failure answers one general text, and the log keeps what failed. A repeated upload with the same Idempotency-Key answers the first answer as it was, in the first request's language. Programs branch on the status, on code when a room refuses the caller (Identity) or a suspended tenant's host refuses everything (suspended, Central admin), and on issues[].code for rejected map packages (Map format), never on the text. A missing or malformed parameter is a 400 problem naming it. The OAuth endpoints are the exception: they answer with the error bodies of RFC 6749, which OAuth clients expect, and whose error_description stays English, as the RFC allows it ASCII characters only; the partner API's Connect errors stay English too, as they answer programs (Partner API).
| Status | Meaning |
|---|---|
| 400 | the request is malformed |
| 401 | no valid access token |
| 403 | the account may not do this |
| 404 | nothing is here, or nothing this account may see |
| 409 | the request conflicts with the current state, such as an overlapping meeting |
| 413 | the upload is too large |
| 422 | the request is well formed but invalid |
| 429 | a rate limit was reached; Retry-After says when to try again |
Pagination
A list answers {"items": [...], "next": "..."}. next is an opaque cursor, absent on the last page; the client passes it back as cursor. limit sets the page size, 50 by default and 100 at most.
Idempotency
Uploads take an Idempotency-Key header. Repeating a request with the same key within 24 hours returns the first successful answer instead of doing the work twice, so a client can retry an upload whose answer was lost; a refused upload is checked again. Two requests with one key at the same moment may both run.
Recordings
GET /api/v1/recordings lists the recordings an account reaches, the newest first, of one world with world: an admin of the tenant reaches every recording, anyone else those it recorded and those of the worlds it administers or owns; any other recording is not found. A recording carries its world, the room its recorder stood in, the kind and name of the space, its recorder, its status (starting, recording, stopping, completed or failed), its times, the length and size of a completed one, why a failed one failed, and expiresAt, when the tenant's retention deletes a completed one (Tenancy). POST /api/v1/recordings/{recording}/download answers where the browser downloads the file of a completed recording: a URL of the S3 bucket, signed for at least an hour, or with the disk backend the server's own /api/v1/recordings/{recording}/file?ticket=…, whose ticket works once within five minutes, because a browser's navigation cannot carry the access token; either saves the file as recording-<start>.mp4. DELETE /api/v1/recordings/{recording} deletes a recording that is over, with its file, and answers 409 for one under way. Starting, deleting and the retention's deletions go to the audit log as recording.started, recording.deleted and recording.expired. Recordings are started over the realtime connection.
Chat channels
GET /api/v1/worlds/{world}/chat/channels lists the channels of a world the caller sees, its default channel first and the others by name: the public ones, the private ones it is in, and every private one for the world's admins and owners and the tenant's admins, who manage them. Each tells whether the caller is in it, until when it is muted there, and how many messages it has not read. POST on the same path creates a channel with a name unique in the world whatever its case (409 otherwise), of at most 80 characters, a topic of at most 250 and a visibility, public or private, for those who manage the world's channels; the creator is in it. PATCH /api/v1/chat/channels/{channel} renames a world's channel, changes its topic or archives it with archived, except the default channel, which is never archived. POST .../join and POST .../leave put the caller in a public channel and take it out of any channel but the default one; GET .../members pages the accounts in a channel, and PUT and DELETE .../members/{user} add a member of the world to a private channel or remove one, for its creator and those who manage the world's channels. PUT .../mutes/{user} with an until time keeps an account from posting in a room's or a world's channel, and DELETE lifts it, for the world's admins and owners and the tenant's admins. POST /api/v1/chat/direct with a userId finds or opens the caller's direct conversation with that account, named after it, for two signed-in accounts that are active members of a common world and do not block each other; anyone else is answered 403, and a conversation with oneself 400. GET /api/v1/chat/messages/{message}/thread pages a thread's replies newest first under its first message, which tells whether the caller follows the thread, and PUT and DELETE .../follow follow it and stop following it, for an account that reads the channel; any other message is not found. In the history, a first message tells how many replies its thread has, when the last came, whether the caller follows the thread and how many replies of others it has not read; the timeline leaves out the replies that stay in their threads. Messages themselves travel over the realtime connection; creating, changing and member changes go to the audit log as chat_channel.created, chat_channel.updated, chat_channel.member_added, chat_channel.member_removed, chat_channel.muted and chat_channel.unmuted.
Chat search
GET /api/v1/chat/search?q=… finds the messages of the channels, threads and direct conversations the caller reads at that moment, as the chat access rule says, whose words begin with every word searched, newest first, 50 at a time and at most 100, without deleted messages and system lines; channel keeps to one channel. Words are letters and digits in any language; a search needs at least two of them in all, and its first eight words count. Each result carries the message, its channel's id and kind, and the name the searcher knows it by: a world's channel's name, the room's, or the other account of a direct conversation. Postgres matches the words with its simple configuration, which stems nothing.
Chat attachments
POST /api/v1/chat/attachments uploads a file as the multipart field file, of at most 25 MiB, 413 above, and answers its id, name, mimeType, as its content shows, sizeBytes and url. The upload is its uploader's alone until a message names it in attachment_ids, within a day, after which it is swept. GET /api/v1/chat/attachments/{attachment} serves the file, with the access token, to accounts that read the channel of its message, and to its uploader before a message took it; anyone else, and a file of a deleted message, is not found. PNG, JPEG, GIF and WebP images come inline, other files as attachments, under X-Content-Type-Options: nosniff; with the s3 backend the answer redirects to a URL the bucket signs for at least an hour. A file goes with its message's deletion.
Room lists
GET /api/v1/worlds/{world}/rooms answers the rooms of a world to an account that one of them admits, the first 100 by slug, for the room list; without a token it answers 401, to an account no room of the world admits 403, and for an unknown world 404. Each item names the room and says whether it admits the caller (open), by the rules of entry (Identity): bans, a meeting's guest kept to the meeting's room, and the room's policy, but not its capacity. people counts the accounts with a player in it, as the processes that run the rooms published them in the last 15 seconds. description and picture are its map's, from the layout's metadata (Map format): picture is an absolute URL as the map gives it, or the address under the site at which the map's files serve a picture in its package.
World members
The tenant's admins manage who belongs to a world under /api/v1/admin/worlds/{world}/members (Tenancy). GET pages the active members in the order of their account ids, each with its name, its email address where the account has one, its role (guest, member, editor, admin or owner), its tags and when it joined. POST with an email, a role and tags makes the tenant's registered account with that address a member, or changes the role and tags of one that is a member already; an address no registered account uses is answered 404 with a problem that names it. PUT .../members/{user} with a role and tags does the same for an account by its id, and DELETE ends a membership, 404 for an account that has none; players already in the world's rooms stay until they leave. A member has at most 20 tags of 1 to 32 characters, and a refusal explains itself in the request's language. Every change goes to the audit log as member.set or member.removed, as the partner API's (Partner API) do.
Bans and the audit log
GET /api/v1/admin/worlds/{world}/bans lists the bans that keep accounts out of a world's rooms now, the newest 200 first, each with the account's id, its name and its email address where it has one, the reason, the moderator's name while that account exists, when it began and, for a ban that ends, expiresAt. Moderators ban in the rooms. DELETE .../bans/{user} lifts every ban of an account from the world, 404 for an account the world does not ban, and the account enters the world's rooms again; the lifting goes to the audit log as member.unbanned.
GET /api/v1/admin/audit-events pages the tenant's audit log, the newest first, 50 events at a time unless limit asks for up to 200, continued with cursor from next. family keeps the actions whose first part it names, such as member for member.banned. Each event carries its id, occurredAt, its action, the kind and id of its subject, its detail as the change recorded it, empty for an action without one, the IP address it came from, and its actor: user or apiKey with its id and its name while it exists, or platform for the server's own jobs. A malformed cursor or family is answered 422.
Background images
A tenant's admins add the images its visitors may show behind them on camera with POST /api/v1/admin/backgrounds?name=…, the image as the body: PNG, JPEG or WebP, as its content shows, of up to 2 MiB, 413 above; a name has 1 to 100 characters, its spaces folded. GET /api/v1/admin/backgrounds lists them by name and DELETE /api/v1/admin/backgrounds/{background} removes one; changes go to the audit log as background.added and background.removed. Each answers its id, name, file, the image's name by the SHA-256 of its content, url and createdAt. Visitors read the same list with GET /api/v1/backgrounds on the tenant's hosts, without an access token, and fetch an image from GET /api/v1/background-images/{file}, cacheable for a year; a removed background's image answers 404 unless another background shows the same image. The images live in object storage under tenants/<tenant id>/backgrounds/ with a file_object row of kind background_image.
Admin messages
POST /api/v1/worlds/{world}/messages sends a message to the players of a world, or of one of its rooms with roomId: a text of at most 1000 characters, plain text whose web addresses the client turns into links, or the audioId of an audio message uploaded before and not expired. It answers 202 once the rooms have the message; only the players there at that moment receive it, and nothing keeps it for later. The admins and owners of the world and the admins of the tenant send; anyone else is answered 403. POST /api/v1/worlds/{world}/audio-messages uploads an audio message as the multipart field file, for the same senders: an MP3, Ogg or WAV file, as its content shows, of at most 10 MiB, 413 above. The server keeps it for a week and answers its id and expiresAt. GET /api/v1/audio-messages/{audio} plays it on the site of its tenant without an access token, because an audio element cannot send one, so whoever holds its address plays it until it expires: a redirect to a URL of the S3 bucket signed for at least an hour, or with the disk backend the file itself. Sending and uploading go to the audit log as notice.sent, with the text or the audio message, and audio_message.uploaded.
Calendars
A registered account connects its calendars under /api/v1/me/calendars and reads its day at GET /api/v1/me/agenda, as Extensions describes: POST starts a connection and answers the provider's address, the platform's GET /api/v1/calendars/callback sends the browser back with a claim, and POST /api/v1/me/calendars/claim connects it for the account that started it, 403 for any other, 404 once spent. DELETE /api/v1/me/calendars/{connection} removes one. The agenda takes the day's date, start and end, refuses a day that is not 23 to 25 hours long with 422, a tenant whose flag calendars is off with 403, and an eleventh reading in a row with 429 and Retry-After: 30. A meeting's calendar file is GET /api/v1/meetings/{meeting}/calendar for members of its world, and GET /api/v1/invites/{token}/calendar for the holder of its link, both text/calendar.
License
The platform's license is kept at /api/v1/admin/license on a single-tenant platform, by the tenant's admins, and at /api/v1/operator/license on a multi-tenant one, by operators: GET reads it, PUT activates a key, POST .../check renews the activation, DELETE releases the key (Licensing). While the license is not active, GET /api/v1/resolve answers 403 with the code unlicensed.
Authentication
Operations that need an account take the access token as Authorization: Bearer <token>. Operations under /api/v1/admin also need the tenant's admin role and answer 403 without it. Tokens never travel in a URL. A missing, expired or revoked token is answered 401 with WWW-Authenticate: Bearer error="invalid_token"; the client refreshes and retries, as Identity describes.
Found a mistake? Tell us.