Running a meeting from your own application
This is everything your application does once it can prove who it is. Create a meeting, invite people, get each of them a join link, run the meeting, end it, receive the event, and read back what the meeting produced.
You need a credential first. Authenticating your application
is that document; every request below carries the token it shows you how to
sign, in an Authorization: Bearer header. Nothing below uses a browser
session or a cookie.
A call needing a scope your token does not carry is refused with 403 scope_not_granted, and the refusal names the scope that was wanted.
You create no accounts in this platform. Not for the organiser, not for the invitees, not for anyone. The people on your meetings are your users, and you name them here the way you already know them — a name and an email address — each carrying your own identifier for them. Nobody signs up, nobody is provisioned, and there is no user list of ours to keep in step with yours. If your product has fifty thousand users, this platform holds none of them.
Where somebody genuinely does hold an account here — a colleague of yours who administers the workspace — you can name them instead, and that is a choice rather than a requirement.
The whole journey in one page
| # | You want to | Call | Scope |
|---|---|---|---|
| 1 | create a meeting and invite everyone | POST /v1/meetings |
meetings:write |
| 2 | add somebody to a meeting you already made | POST /v1/meetings/{id}/participants |
meetings:write |
| 3 | see who is already on it | GET /v1/meetings/{id}/participants |
meetings:read |
| 4 | get one person a way in | POST /v1/meetings/{id}/join-tokens |
tokens:mint |
| 5 | read it back, list, edit | GET/PATCH /v1/meetings… |
meetings:read / :write |
| 6 | call it off | POST /v1/meetings/{id}/cancel |
meetings:write |
| 7 | say it is over | POST /v1/meetings/{id}/end |
meetings:write |
| 8 | be told it is over | POST /v1/webhooks once, then your endpoint receives |
webhooks:manage |
| 9 | fetch what it produced | GET /v1/meetings/{id}/artefacts |
artefacts:read |
| 10 | see what happened | GET /v1/audit-events |
audit:read |
A subject is one of two shapes, everywhere one is accepted — creating a
meeting, adding a participant, minting a join token:
{"type": "user", "user_id": "0193…"}
{"type": "guest", "name": "Ada Byron", "email": "ada@example.com"}
A user names a member of your tenant and carries their identity. A guest
names somebody who has none. Every example below uses guest; user takes the
same place in the same member.
1. Create the meeting and its invitees, in one call
curl -sS -X POST https://<host>/v1/meetings \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"title": "Quarterly review",
"vertical": "meeting",
"scheduled_at": "2026-09-05T14:00:00Z",
"timezone": "Europe/London",
"duration_minutes": 45,
"lobby": true,
"organiser": {"type": "guest", "name": "Rosa Kaur", "email": "rosa@yourcompany.example"},
"organiser_external_ref": "your-user-88121",
"participants": [
{"subject": {"type": "guest", "name": "Ada Byron", "email": "ada@example.com"}, "external_ref": "your-user-4471"},
{"subject": {"type": "guest", "name": "Ivan Petrov", "email": "ivan@example.com"}, "external_ref": "your-user-9002"}
]
}'
201, and the body carries the occurrence and every participant you just
created:
{
"id": "0193…", "title": "Quarterly review", "status": "scheduled",
"sequence": 0, "scheduled_at": "2026-09-05T14:00:00Z",
"timezone": "Europe/London", "duration_minutes": 45, "lobby": true,
"capacity": 20, "occurrences": 1,
"entry_url": "https://<host>/m/0193…",
"organiser": {"type": "guest", "user_id": null, "name": "Rosa Kaur",
"email": "rosa@yourcompany.example", "external_ref": "your-user-88121"},
"participants": [
{"id": "0193…", "meeting_id": "0193…", "name": "…", "meeting_role": "participant",
"rsvp": "pending", "external_ref": "your-user-4471"},
…
]
}
The body, member by member
| Member | Required | Value |
|---|---|---|
title |
yes | up to 300 characters |
vertical |
yes | meeting, conference, webinar or learning. It fixes the meeting's capacity and which features the room offers, and a vertical your tenant is not entitled to is 403 vertical_not_licensed. |
scheduled_at |
yes | an instant, ISO 8601 |
timezone |
yes | an IANA name, such as Europe/London. The occurrence is scheduled in it, so a series survives a daylight-saving change. |
duration_minutes |
yes | a positive integer |
organiser |
yes, for an application | who owns the meeting: {"type": "guest", "name": …, "email": …} or {"type": "user", "user_id": …}. A guest organiser needs no account here. |
organiser_external_ref |
no | your own identifier for the organiser |
description |
no | up to 5000 characters |
lobby |
no | true by default. With a lobby, somebody who is not on the roster waits to be admitted by a moderator; without one, they enter directly. |
registration |
no | {"enabled": false, "approval": "auto"} by default. With enabled: true, only registered participants are admitted and anyone else is 403 registration_required. approval is auto or manual. |
recurrence |
no | see below |
participants |
no | up to 100 entries; each entry is described in its own table below |
organiser_user_id |
no | superseded by organiser; still works, still means a member of your tenant. Sending both is 422. |
A participant, member by member
Each entry in participants — and the whole body of
POST /v1/meetings/{id}/participants, which takes exactly the same shape.
| Member | Required | Value |
|---|---|---|
subject |
yes | who they are: {"type": "guest", "name": …, "email": …} for one of your own people, or {"type": "user", "user_id": …} for a member of your tenant |
meeting_role |
no | organiser, moderator or participant. Defaults to participant. |
external_ref |
no | your identifier for this person, carried through untouched. Up to 200 characters, and no two people on one meeting may share one. |
Anything else is refused, by name, with a JSON Pointer to it. There is no member this API accepts and ignores.
capacity is read-only. It appears in the response and is a property of
the vertical, not something you set: meeting admits 20, learning 100,
conference and webinar 500. A meeting that fills answers 409 capacity_reached, and the way to a larger room is a different vertical.
entry_url is the link you send people. It is the one door: whoever opens
it identifies themselves there and is issued their own token. It is stable for
the meeting's whole life, carries no credential, and is safe in your own
invitation email, your calendar entry, or your application's interface.
It is not the same as the join_url returned by
POST /v1/meetings/{id}/join-tokens. That one carries a signed token for one
named person and expires in ten minutes; forwarding it hands somebody else that
person's way in. If you are unsure which you want, you want entry_url.
It is one transaction. The meeting and every invitee commit together or none of them does. If one invitee is wrong, nothing is created — not the meeting, not the other invitees, not an invitation email.
Refusals name the invitee by position. A 422 carries an errors array
whose pointers are RFC 6901 JSON Pointers into your request body:
{"code": "validation_failed", "errors": [
{"pointer": "/participants/2/subject/user_id", "code": "invalid",
"message": "This user is not an active member of this tenant."}
]}
Every bad entry is reported, not just the first, so fixing a list of fifty does not take fifty round trips.
The organiser can be one of your users, not one of ours
Your application is not a person, so it has to say who owns each meeting. It does not have to create an account here first.
"organiser": {"type": "guest", "name": "Rosa Kaur", "email": "rosa@yourcompany.example"}
That person is the organiser in full: the meeting's owner, the calendar
ORGANIZER, and the address the invitation comes from. No account, no
membership, nothing to set up. It is the same subject shape you already use
for participants and for join links.
Where the organiser genuinely is one of ours, name them instead:
"organiser": {"type": "user", "user_id": "0193..."}
Omit the member entirely and you get 422 organiser_required — your
application named nobody at all. type: user naming somebody who is not an
active member of your tenant is 422 organiser_not_member; a guest organiser
cannot get that refusal, because there is nothing to be a member of.
A signed-in user calling the same endpoint may omit the organiser and becomes
the organiser themselves. Naming someone else needs the tenant_admin
role — that rule is about one person scheduling in another person's name, and
it does not apply to your application.
organiser_user_id still works exactly as it did and means
organiser: {"type": "user", "user_id": …}. Sending both is 422: they name
the same thing, and we would rather refuse than guess which you meant.
Your own identifier on every person
Every participant, and the organiser, can carry external_ref — your
identifier for that person in your system.
{"subject": {"type": "guest", "name": "Ada Byron", "email": "ada@example.com"},
"external_ref": "your-user-4471"}
We never parse it, never validate it, never assume a format, and never trim it. It comes back byte for byte on every read, it is in the audit trail, and it rides every event that names a participant — so you can match what we tell you to your own records without keeping a table of our identifiers.
Two rules, and only two:
- At most 200 characters. Longer is
422, pointing at the entry. We refuse rather than truncate, because a truncated identifier matches nothing in your system while looking as though it should. - Unique within one meeting. Two people on the same meeting may not share a
reference — that is the ambiguity the member exists to remove. A duplicate is
422, naming the second entry by position. Any number of people may carry no reference at all.
Recurring meetings
Add recurrence. Every occurrence is its own resource with its own id, its own
participants and its own join links. The occurrence is the isolation boundary
everywhere in this platform: standing on one is not standing on another.
"recurrence": {"freq": "weekly", "interval": 1, "by_day": ["tu"], "count": 8}
Exactly one of until / count. freq is daily, weekly or monthly.
That closed set is the supported subset, and anything outside it is 422 rule_outside_subset rather than a silently narrower meaning.
Your invitees are attached to every materialised occurrence, and are mailed
once, for the first. occurrences in the response says how many were created;
GET /v1/meetings?series_id=… lists them.
At most 100 invitees per call
Invite the rest with POST /v1/meetings/{id}/participants, whose whole body is
one entry from the table above — subject, meeting_role and external_ref,
with the same meanings.
2. Idempotency-Key
It is required on every creating POST, and this platform stores it with the
response it produced.
- The same key with the same body returns the original response unchanged:
same status, same body, same ids, plus
Idempotency-Replayed: true. Nothing is created a second time. - The same key with a different body is
409 idempotency_key_reused. The key names one intention, and answering with the first request's meeting would hide the mistake behind an apparent success. - The same key while the first request is still running is
409 idempotency_request_in_flight. Nothing was created by the second attempt. Wait and retry; you will get the stored response. - A request that failed spends no key. Nothing was created, so the same key is free. Fix the body and send it again with the same key.
Stored keys answer replays for 24 hours on this deployment and then expire.
The safe pattern for a network timeout is to retry with the same key. You either get the meeting you already made, or you make it now.
3. Get someone a join link
curl -sS -X POST https://<host>/v1/meetings/$MEETING/join-tokens \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"subject": {"type": "guest", "name": "Ada Byron", "email": "ada@example.com"}}'
{"token": "eyJ…", "join_url": "https://meet…/0193…?jwt=eyJ…",
"role": "participant", "meeting_id": "0193…", "participant_id": "0193…",
"expires_at": "2026-09-05T14:10:00Z"}
Seven rules govern this call.
-
The person must already be on the meeting. A join token names somebody with standing on that occurrence: a participant, or its organiser. Minting for anyone else is
403 subject_not_on_occurrence, whether or not you asked for a role. Invite them first withPOST /v1/meetings/{id}/participants. -
The first mint takes the occurrence live, and that closes the roster.
scheduling.meeting_startedfires on it, and from that momentPOST /v1/meetings/{id}/participantsanswers409 not_editable. Invite everybody before you mint for anybody; afterwards,entry_urlis the way in for somebody who is not on the list. -
One link, one person, one meeting. The token's room claim is that occurrence and nothing else. There is no wildcard.
-
It is short-lived — ten minutes on this deployment. It is a credential to get in, so mint it when the person is about to join, not when you send the invitation.
-
There is a join window. A scheduled occurrence becomes joinable ten minutes before its start and stops being joinable at its end. Create a meeting for next week, ask for a join link, and you get
409 not_joinable_yetcarryingwindow_opens_atandwindow_closes_at. For a link to put in an invitation, useentry_url. -
Capabilities are decided here, not requested. What a participant can do in the room comes from the meeting's vertical and their standing on it. A role above your caller's authority is
403 role_not_permittedrather than a token that quietly grants less than you asked for. -
The room is prepared before the token is signed. Where it cannot be, you get
503 room_not_provisionedand no token: you never receive a link into a room whose admission policy could not be written. This is the only call in this document that depends on the room being reachable; creating, editing and cancelling meetings do not.It carries a
cause:unconfigured(this deployment cannot provision rooms, and retrying will not help),unreachable(worth a retry),refused(the request was answered and the policy was not accepted, which needs an operator).
4. Read, list, edit, cancel, end
GET /v1/meetings list (filter: from, to, status, vertical, series_id)
GET /v1/meetings/{id} one occurrence
GET /v1/meetings/{id}/ics the calendar payload
GET /v1/meetings/{id}/participants who is on it
PATCH /v1/meetings/{id} edit — scheduled state only
POST /v1/meetings/{id}/cancel call it off; participants are notified
POST /v1/meetings/{id}/end it is over
Participants have their own four:
GET /v1/meetings/{id}/participants who is on it
POST /v1/meetings/{id}/participants invite one more
PATCH /v1/meetings/{id}/participants/{participantId} change their meeting role
DELETE /v1/meetings/{id}/participants/{participantId} withdraw the invitation
participantId is the id of a row in the roster, which is what the create
response and GET …/participants return. Inviting somebody already on the
occurrence is 409 participant_exists; change their role with the PATCH
rather than inviting them again.
Sending an external_ref that another person on the same meeting already
carries is a different refusal — 409 external_ref_taken. The two are not the
same and the remedy is not the same: participant_exists means the person is
already invited and there is nothing to do, while external_ref_taken means
the person is not invited and your reference belongs to somebody else.
A fifth acts on a visitor:
POST /v1/meetings/{id}/participants/{participantId}/promote tokens:mint
A visitor is minted by asking for "role": "visitor" on the join-token call.
Promoting one supersedes their visitor token with a participant token. A
participant holding no live visitor token is 409 not_promotable, a vertical
that admits no visitors is 403 visitor_tier_not_offered, and promoting
without moderator authority on the occurrence is 403 promotion_not_permitted.
The closed sets a response carries.
| Member | Values |
|---|---|
status on a meeting |
scheduled, live, completed, cancelled, expired. The ?status= filter takes the same five. |
meeting_status, carried by every not_joinable_* refusal |
the same five |
rsvp on a participant |
pending, accepted, declined, tentative |
meeting_role on a participant |
organiser, moderator, participant |
role in a join-token response |
moderator, participant, visitor — what the token grants, which is not the same thing as meeting_role, the person's standing on the occurrence |
data.ended_by on scheduling.meeting_ended |
operation where somebody ended it, window where the platform closed it after its window passed |
vertical |
meeting, conference, webinar, learning |
sequence is load-bearing. It is the RFC 5545 SEQUENCE for the
occurrence, and it advances on every material change. Calendar clients discard
an update whose sequence has not advanced, so if you mirror these meetings into
a calendar of your own, this is the field that tells you your copy is stale.
Cancel is a transition, never a delete. The occurrence stays, with
status: cancelled, because it is the anchor for its audit trail and anything
it recorded. Participants get a METHOD:CANCEL calendar notice with the
incremented sequence.
?scope= on edit and cancel takes this | this_and_future | all for a
recurring series. A value outside that set is refused with 422 rather than
quietly read as this.
A link already issued for a cancelled meeting. No new token is minted
(409 not_joinable_cancelled), and a token already in someone's hands stays
valid until it expires. If that matters to you, cancel and then wait out the
ten-minute token lifetime before assuming nobody can enter.
Four codes cover the four reasons a mint is refused.
| Code | What it means | Carries |
|---|---|---|
not_joinable_yet |
the window has not opened | window_opens_at, window_closes_at |
not_joinable_window_closed |
still scheduled, past its end | window_closes_at |
not_joinable_cancelled |
cancelled | — |
not_joinable_finished |
completed or expired | — |
All four also carry meeting_status. The window opens ten minutes before the
start on this deployment and closes at the occurrence's end; a live
occurrence is joinable throughout.
not_cancellable and not_endable are different codes. A live meeting is
ended; a completed, expired or already-cancelled one is finished. One code
for both would tell you neither.
5. Be told the meeting ended
POST /v1/webhooks register an endpoint
GET /v1/webhooks list them
PATCH /v1/webhooks/{id} change events, description, status
DELETE /v1/webhooks/{id} remove one
POST /v1/webhooks/{id}/rotate-secret new signing secret
GET /v1/webhooks/{id}/deliveries what was sent, and what happened
POST /v1/webhooks/{id}/deliveries/{delivery}/redeliver send one again
All of them need webhooks:manage, or a tenant_admin session.
Register an endpoint
curl -sS -X POST https://<host>/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/svc",
"events": ["scheduling.meeting_ended", "artefact.registered"]}'
| Member | Required | Value |
|---|---|---|
url |
yes | https://, on a name that resolves publicly, on port 443 or 8443. No other port is accepted. |
events |
yes | at least one exact event name or family prefix |
description |
no | up to 300 characters, for your own reference |
The 201 carries the signing secret once. Store it now; no endpoint returns it
again. What you can read afterwards is secret_fingerprint, the first eight
hex of its sha256 — enough to answer "is this the secret the deployment holds?"
and not enough to sign anything. If the secret is lost, POST /v1/webhooks/{id}/rotate-secret issues a new one.
Subscribe with an exact event name or a family prefix. A prefix is an
event name truncated at a dot, with the dot kept and nothing after it:
scheduling. takes every event whose name begins with it, and artefact. and
consent. do the same for theirs. There is no wildcard character, and
scheduling without the dot is not a prefix. An entry that matches no event this deployment emits is refused
at registration with 422 event_unknown and its index, so that a typo cannot
become a subscription that never fires. The exact names are in
GET /v1/integrator/discovery under webhooks.events; the prefixes are formed
from them by this rule.
Your address must be https://, on port 443 or 8443, on a public name.
Loopback, private ranges, link-local and the cloud metadata address are
refused, as is any other port. The check runs again at delivery time, after
DNS, so a name that starts resolving privately later becomes a delivery failure
rather than a registration refusal.
What arrives
POST https://example.com/svc
X-Svc-Signature: t=1788091200,v1=aGVsbG8…
X-Svc-Event: scheduling.meeting_ended
X-Svc-Event-Id: 0193…
X-Svc-Sequence: 41
X-Svc-Delivery-Attempt: 1
Content-Type: application/json
{
"id": "0193…",
"event": "scheduling.meeting_ended",
"sequence": 41,
"occurred_at": "2026-09-05T14:47:03.118000Z",
"tenant_id": "0193…",
"correlation_id": "0193…",
"data": {
"meeting_id": "0193…", "series_id": null,
"started_at": "2026-09-05T14:02:11Z",
"ended_at": "2026-09-05T14:47:03Z",
"ended_by": "operation"
}
}
Payloads are thin: identifiers and the few members that make the event actionable, never a snapshot of the meeting. Fetch current state with your own credential when you need it.
Verify the signature
v1 is the base64 of an HMAC-SHA256 over <t>.<raw request body>, keyed with
your endpoint secret.
import base64, hashlib, hmac, time
def verify(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
if "t" not in parts or "v1" not in parts:
return False # no v1 signature to check
if abs(time.time() - int(parts["t"])) > tolerance:
return False # bounds replay
expected = base64.b64encode(
hmac.new(secret.encode(), f'{parts["t"]}.'.encode() + raw_body, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, parts["v1"])
Two things decide whether this works:
- The whole secret is the key,
whsec_prefix included. It is part of the key, not a label. Strip it and your digest will never match. - The tolerance is 300 seconds on this deployment, published at
GET /v1/integrator/discovery→webhooks.signature_tolerance_seconds.
Sign over the raw bytes you received. Re-serialising the JSON first changes whitespace or key order, and the signature will not match.
Ordering, duplicates, and gaps
- De-duplicate on
id. Delivery is at-least-once, and the same delivery can arrive twice. - Order on
occurred_at. That is the transition's own time. Arrival order is not causal order: ameeting_endedcan reach you before themeeting_startedthat precedes it. A consumer that trusts arrival order corrupts its own state. sequencedetects a delivery you never received. It is per endpoint, starts at 1, and never skips. Seen 41 and 43? Fetch 42 fromGET /v1/webhooks/{id}/deliveries. It is not an ordering: two deliveries are dispatched concurrently, and a higher sequence can arrive first.- Retries keep the sequence.
X-Svc-Delivery-Attemptcounts the tries;sequenceidentifies the delivery.
When a delivery does not arrive
Every attempt is recorded and readable:
curl -sS -H "Authorization: Bearer $TOKEN" \
https://<host>/v1/webhooks/$ENDPOINT/deliveries
{"data": [{
"id": "0193…", "event": "scheduling.meeting_ended", "sequence": 41,
"status": "pending", "attempt": 2, "next_retry_at": "2026-09-05T15:02:00Z",
"attempts": [
{"attempt": 1, "outcome": "http_error", "response_status": 502, "duration_ms": 118, "attempted_at": "…"},
{"attempt": 2, "outcome": "timeout", "error": "No response within 10s.", "attempted_at": "…"}
]
}]}
outcome distinguishes four different failures: http_error (you answered,
badly), timeout (you did not answer), connection_failed (you could not be
reached), target_not_permitted (your hostname now resolves to a private
address). Your own response body is never stored.
Backoff is 30s, 2m, 10m, 1h, 4h, up to 6 attempts inside 24 hours. An endpoint
that fails 20 deliveries in a row is auto-disabled and your tenant
administrators are told; PATCH /v1/webhooks/{id} {"status":"active"}
re-enables it and clears the counter.
Replay one by hand with POST /v1/webhooks/{id}/deliveries/{delivery}/redeliver. It re-sends the recorded
transition — the same payload, the same occurred_at, a new delivery id and
a new sequence — and never re-runs the operation that produced it, which is
what makes replaying safe. A delivery that has not finished yet is 409 delivery_not_replayable.
6. Read what the meeting produced
GET /v1/meetings/{id}/artefacts recordings, transcripts, summaries artefacts:read
GET /v1/meetings/{id}/consents the consent decisions of record consents:read
GET /v1/audit-events?action=… everything that happened audit:read
action takes an exact name or a dotted prefix. Your own tenant's events, and
nobody else's.
Listing routes page with ?limit= and ?cursor=. limit is between 1 and 100
on this deployment; outside that it is refused, with limit_invalid beside the
pointer. The response carries pagination.next_cursor, and a null cursor
means there is no next page.
An event your application caused carries no user as its actor, because no person did it, and names your application in its payload instead. That is what an auditor will see.
7. What happens when things go wrong
| What happened | What you get | What is left behind |
|---|---|---|
| The room cannot be reached when you create a meeting | nothing — creating a meeting does not depend on it | the meeting |
| The room cannot be reached when you mint a join link | 503 room_not_provisioned |
no token; nobody gets a link into a room that was not prepared |
| An invitee's address is malformed | 422 naming /participants/{n}/subject/email |
nothing at all |
Two invitees in one request carry the same external_ref |
422 naming /participants/{n}/external_ref |
nothing at all |
You invite somebody with an external_ref another person on the meeting holds |
409 external_ref_taken |
nothing; the meeting and its roster are untouched |
You send both organiser and organiser_user_id |
422 |
nothing at all |
| An invitee's address is valid and undeliverable | 201 — nobody can tell in advance |
the meeting stands; the send is retried and then abandoned. No event reports it, and no call reads it back |
| You retry a create that timed out | the original 201, Idempotency-Replayed: true |
one meeting |
| You reuse a key for a different meeting | 409 idempotency_key_reused |
nothing |
| Two of your workers race with one key | one 201, one 409 idempotency_request_in_flight |
one meeting |
| Someone holds a join link and you cancel | no new tokens (409 not_joinable_cancelled) |
the live token works until it expires, ten minutes |
| You mint a link for a meeting whose window has not opened | 409 not_joinable_yet, naming the instant it opens |
nothing |
| Your webhook endpoint is down | 2xx to the caller who caused the event |
a pending delivery with next_retry_at, retried on the backoff |
| Your webhook endpoint returns 500 | as above | an attempt row with outcome: http_error and your status |
| Your webhook endpoint is slow | as above | an attempt row with outcome: timeout |
| Your clock is wrong | 401 token_expired or token_not_yet_valid, naming both clocks and the tolerance |
nothing |
| Your token lacks a scope | 403 scope_not_granted, naming the scope |
nothing |
| You use another tenant's id | 404, never 403 |
nothing — and the answer discloses nothing about what exists there |
Every refusal has its own stable code. Branch on it rather than parsing a
sentence.
A member this API does not define is refused, not ignored. Send
{"passcode": "1234"} on POST /v1/meetings and you get 422 naming
/passcode, because this platform has no passcode and a 201 with your
setting quietly dropped would be worse than an error. The same applies to a
member sent in the wrong place: role inside subject is refused at
/participants/0/subject/role rather than silently doing nothing.
Boundaries of this API
Stated so you do not build against something that is not there.
- No anonymous join. Every subject is named. There is no path that issues a token to nobody.
scheduling.meeting_startedfires when the first join token is minted, not when a person actually enters the room. It means that somebody has asked for a way in.meeting_endedis the explicit end, or the platform closing a meeting whose window has passed;data.ended_bysays which.- No per-occurrence RSVP on a recurring series. An invitee answers the first occurrence's invitation. Later occurrences carry their participant rows and no separate invitation.
- The event catalogue in
/v1/integrator/discoveryis the complete list. There is no event for moderation, for credential changes, or per join.