Reference
Meetings
22 routes, generated from Morse’s own OpenAPI document. Every path below hangs off the base URL, and every one needs the bearer header.
The examples are built from each route’s schema, so the shapes and types are exactly what the API declares. The values are illustrative, and no one has run them.
Unauthenticated on purpose: the join page must render for someone who is not signed in yet. Returns no title (ADR-009).
/m/{code}i is the per-recipient invite token. Presented and valid, it earns one
extra fact: who invited them. This is provenance, not verification — a
forwarded email forwards the token, exactly as a calendar invite does — and
the name is no more than the host's, which every visitor already sees.
Carries how many are inside, never who (plan 018 D4, narrowed in review of
#132). Read once when the join page loads, not polled: the question is whether
there is anyone there to let you in, which is answered on arrival. A repeated
read would make a forwarded link a presence feed, sampled without anything on
the host's screen to show it happened.
GET/m/{code}
curl "$MORSE/m/4KJ9P2" \
-H "Authorization: Bearer $MORSE_TOKEN"{
"code": "4KJ9P2",
"host_name": "Priya Shah",
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"invited_by_name": "Priya Shah",
"recording_enabled": true,
"status": "…",
"transcription_enabled": true
}Your meetings, newest first, at most 100: ones you host, and ones you were in, invited to or waited for.
/meetingsGET/meetings
curl "$MORSE/meetings" \
-H "Authorization: Bearer $MORSE_TOKEN"[
{
"archived": true,
"at_risk": true,
"attendee_count": 3,
"attendees": [
{
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"name": "Priya Shah"
}
],
"booked": true,
"booked_by_me": true,
"booked_timezone": "Asia/Kolkata",
"booking_note": "…",
"calendar_sync_status": "synced",
"code": "4KJ9P2",
"copilot_enabled": true,
"created_at": "2026-10-02T10:30:00+05:30",
"duration_minutes": 30,
"ended_at": "2026-10-02T10:30:00+05:30",
"generated_title": "…",
"has_whiteboard": true,
"host_id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"host_name": "Priya Shah",
"host_title": "…",
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"in_room": true,
"internal": true,
"invite_counts": 3,
"invitee_names": [
"Priya Shah"
],
"invitees": [
{
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"name": "Priya Shah"
}
],
"is_host": true,
"locked": true,
"my_proposed_start": "2026-10-02T10:30:00+05:30",
"my_response": "…",
"notes_ready": true,
"notes_viewed": true,
"notes_writing": true,
"on_calendar": true,
"open_suggestions": 1,
"record_access": "host",
"recording_available": true,
"recording_enabled": true,
"scheduled_at": "2026-10-02T10:30:00+05:30",
"started_at": "2026-10-02T10:30:00+05:30",
"status": "…",
"summary_template": "…",
"title": "Pricing review",
"transcription_enabled": true,
"whiteboard_open": true
}
]Create a meeting you host, for now or scheduled (scheduled_at, a length and a timezone).
/meetingsOptionally invite people in the same call with emails; each comes back under invites with
how it was delivered. Returns the meeting with its join code.
POST/meetings
curl -X POST "$MORSE/meetings" \
-H "Authorization: Bearer $MORSE_TOKEN" \
-H "x-timezone: Asia/Kolkata" \
-H "Content-Type: application/json" \
-d '{ "booked_timezone": "Asia/Kolkata", "duration_minutes": 30, "emails": [ "priya@neuralarc.ai" ], "key_emails": [ "priya@neuralarc.ai" ], "scheduled_at": "2026-10-02T10:30:00+05:30", "title": "Pricing review" }'{
"archived": true,
"at_risk": true,
"attendee_count": 3,
"attendees": [
{
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"name": "Priya Shah"
}
],
"booked": true,
"booked_by_me": true,
"booked_timezone": "Asia/Kolkata",
"booking_note": "…",
"calendar_sync_status": "synced",
"code": "4KJ9P2",
"copilot_enabled": true,
"created_at": "2026-10-02T10:30:00+05:30",
"duration_minutes": 30,
"ended_at": "2026-10-02T10:30:00+05:30",
"generated_title": "…",
"has_whiteboard": true,
"host_id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"host_name": "Priya Shah",
"host_title": "…",
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"in_room": true,
"internal": true,
"invite_counts": 3,
"invitee_names": [
"Priya Shah"
],
"invitees": [
{
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"name": "Priya Shah"
}
],
"invites": [
{
"delivery": "…",
"detail": "…",
"email": "priya@neuralarc.ai",
"kind": "…"
}
],
"is_host": true,
"locked": true,
"my_proposed_start": "2026-10-02T10:30:00+05:30",
"my_response": "…",
"notes_ready": true,
"notes_viewed": true,
"notes_writing": true,
"on_calendar": true,
"open_suggestions": 1,
"record_access": "host",
"recording_available": true,
"recording_enabled": true,
"scheduled_at": "2026-10-02T10:30:00+05:30",
"started_at": "2026-10-02T10:30:00+05:30",
"status": "…",
"summary_template": "…",
"title": "Pricing review",
"transcription_enabled": true,
"whiteboard_open": true
}Read a line of plain English, such as 'pricing review with Arjun Thursday 3pm UK time', into the schedule form's fields.
/meetings/interpretBooks nothing.
POST/meetings/interpret
curl -X POST "$MORSE/meetings/interpret" \
-H "Authorization: Bearer $MORSE_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "text": "Annual plans are billed up front.", "zone": "…" }'{
"copilot": true,
"date": "2026-10-02",
"emails": [
"priya@neuralarc.ai"
],
"locked": true,
"minutes": 30,
"names": [
"Priya Shah"
],
"recording": true,
"template": "…",
"time": "…",
"timezone": "Asia/Kolkata",
"title": "Pricing review",
"transcription": true
}Find a meeting in your own list by its title, its minutes or — for the host — its transcript.
/meetings/searchNever widens what any other route lets you read.
GET/meetings/search
curl "$MORSE/meetings/search?q=pricing" \
-H "Authorization: Bearer $MORSE_TOKEN"[
{
"generated_title": "…",
"match": "title",
"meeting_id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"record_access": "host",
"scheduled_at": "2026-10-02T10:30:00+05:30",
"snippet": "…",
"started_at": "2026-10-02T10:30:00+05:30",
"status": "…",
"title": "Pricing review"
}
]Your meetings list changed. One event type, meetings, and no payload:
the client refetches (events.meetings_topic).
/meetings/streamDeclared above /{meeting_id} so the literal path wins the match.
No catch-up: the page server-renders the list it opens with, and anything
raised between that render and this connection is a gap of milliseconds on a
list the client refetches anyway. **Trigger to add one:** a report of a
meeting that never appeared until a reload.
events is in-process (ADR-007), so this reaches a client only when the
publisher ran on the same API process. That holds at one replica and is the
docstring's stated trigger to build LISTEN/NOTIFY fan-out — until then an
invitee connected elsewhere simply sees the meeting on their next load,
which is the behaviour before this stream existed.
GET/meetings/stream
curl "$MORSE/meetings/stream" \
-H "Authorization: Bearer $MORSE_TOKEN"One meeting: its status, times, host and settings, and whether you may read its notes.
/meetings/{meeting_id}GET/meetings/{meeting_id}
curl "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60" \
-H "Authorization: Bearer $MORSE_TOKEN"{
"archived": true,
"at_risk": true,
"attendee_count": 3,
"attendees": [
{
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"name": "Priya Shah"
}
],
"booked": true,
"booked_by_me": true,
"booked_timezone": "Asia/Kolkata",
"booking_note": "…",
"calendar_sync_status": "synced",
"code": "4KJ9P2",
"copilot_enabled": true,
"created_at": "2026-10-02T10:30:00+05:30",
"duration_minutes": 30,
"ended_at": "2026-10-02T10:30:00+05:30",
"generated_title": "…",
"has_whiteboard": true,
"host_id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"host_name": "Priya Shah",
"host_title": "…",
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"in_room": true,
"internal": true,
"invite_counts": 3,
"invitee_names": [
"Priya Shah"
],
"invitees": [
{
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"name": "Priya Shah"
}
],
"is_host": true,
"locked": true,
"my_proposed_start": "2026-10-02T10:30:00+05:30",
"my_response": "…",
"notes_ready": true,
"notes_viewed": true,
"notes_writing": true,
"on_calendar": true,
"open_suggestions": 1,
"record_access": "host",
"recording_available": true,
"recording_enabled": true,
"scheduled_at": "2026-10-02T10:30:00+05:30",
"started_at": "2026-10-02T10:30:00+05:30",
"status": "…",
"summary_template": "…",
"title": "Pricing review",
"transcription_enabled": true,
"whiteboard_open": true
}Rename, reschedule, re-length. Host only — joining is not gated on membership, so without the created_by guard any account that can reach a meeting could retitle or move it.
/meetings/{meeting_id}**True PATCH semantics** (IMPLEMENTATION.md:622's idiom): model_fields_set
is the only way to tell "field not sent" from "field sent as null", and here
the difference is load-bearing. An absent scheduled_at leaves the time
alone, and an explicit null is refused on a meeting that has one — a booked
meeting is moved or cancelled, never unscheduled. A rename that silently
unscheduled a meeting would be a very expensive bug to find.
An empty or whitespace title still clears back to NULL rather than storing a
blank: NULL is what marks a meeting as unnamed, and the UI derives a label
from its time for those. Storing the derived label would fix it to the
creator's timezone and lose the distinction between named and unnamed.
PATCH/meetings/{meeting_id}
curl -X PATCH "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60" \
-H "Authorization: Bearer $MORSE_TOKEN" \
-H "x-timezone: Asia/Kolkata" \
-H "Content-Type: application/json" \
-d '{ "booked_timezone": "Asia/Kolkata", "duration_minutes": 30, "scheduled_at": "2026-10-02T10:30:00+05:30", "title": "Pricing review", "transcription_enabled": true }'{
"archived": true,
"at_risk": true,
"attendee_count": 3,
"attendees": [
{
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"name": "Priya Shah"
}
],
"booked": true,
"booked_by_me": true,
"booked_timezone": "Asia/Kolkata",
"booking_note": "…",
"calendar_sync_status": "synced",
"code": "4KJ9P2",
"copilot_enabled": true,
"created_at": "2026-10-02T10:30:00+05:30",
"duration_minutes": 30,
"ended_at": "2026-10-02T10:30:00+05:30",
"generated_title": "…",
"has_whiteboard": true,
"host_id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"host_name": "Priya Shah",
"host_title": "…",
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"in_room": true,
"internal": true,
"invite_counts": 3,
"invitee_names": [
"Priya Shah"
],
"invitees": [
{
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"name": "Priya Shah"
}
],
"is_host": true,
"locked": true,
"my_proposed_start": "2026-10-02T10:30:00+05:30",
"my_response": "…",
"notes_ready": true,
"notes_viewed": true,
"notes_writing": true,
"on_calendar": true,
"open_suggestions": 1,
"record_access": "host",
"recording_available": true,
"recording_enabled": true,
"scheduled_at": "2026-10-02T10:30:00+05:30",
"started_at": "2026-10-02T10:30:00+05:30",
"status": "…",
"summary_template": "…",
"title": "Pricing review",
"transcription_enabled": true,
"whiteboard_open": true
}Host only, and only once it is over. The meeting goes from everyone's
list and every read of it answers 404, but nothing is erased: its
transcript, summary, recordings and send record all stay, behind
deleted_at. So a summary still being written, or a recording still
uploading, finishes into a meeting nobody can open rather than failing.
/meetings/{meeting_id}Deleting twice is not an error: a double click or a second tab asked for what has already happened. An upcoming meeting is cancelled rather than deleted: the people invited to it hold a link, and "cancelled" is an answer that link can give. A live one has to end first, or the room would outlive the meeting that says who may be in it.
DELETE/meetings/{meeting_id}
curl -X DELETE "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60" \
-H "Authorization: Bearer $MORSE_TOKEN"Archive a meeting out of your own list: tidying, not deleting.
/meetings/{meeting_id}/archiveNothing
else changes — the meeting, its notes and everyone else's list stay as they
were, a link still opens it, and DELETE brings it back. Host or attendee.
Idempotent.
PUT/meetings/{meeting_id}/archive
curl -X PUT "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60/archive" \
-H "Authorization: Bearer $MORSE_TOKEN"Bring an archived meeting back into your list.
/meetings/{meeting_id}/archiveIdempotent.
DELETE/meetings/{meeting_id}/archive
curl -X DELETE "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60/archive" \
-H "Authorization: Bearer $MORSE_TOKEN"Try again to put this meeting on Google Calendar.
/meetings/{meeting_id}/calendar-syncThe whole of D21: rather than a retry queue and a worker for a failure that will happen a handful of times a year at this size, the state is made visible and the fix is a button. The host already has the page open.
POST/meetings/{meeting_id}/calendar-sync
curl -X POST "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60/calendar-sync" \
-H "Authorization: Bearer $MORSE_TOKEN"{
"archived": true,
"at_risk": true,
"attendee_count": 3,
"attendees": [
{
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"name": "Priya Shah"
}
],
"booked": true,
"booked_by_me": true,
"booked_timezone": "Asia/Kolkata",
"booking_note": "…",
"calendar_sync_status": "synced",
"code": "4KJ9P2",
"copilot_enabled": true,
"created_at": "2026-10-02T10:30:00+05:30",
"duration_minutes": 30,
"ended_at": "2026-10-02T10:30:00+05:30",
"generated_title": "…",
"has_whiteboard": true,
"host_id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"host_name": "Priya Shah",
"host_title": "…",
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"in_room": true,
"internal": true,
"invite_counts": 3,
"invitee_names": [
"Priya Shah"
],
"invitees": [
{
"id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"name": "Priya Shah"
}
],
"is_host": true,
"locked": true,
"my_proposed_start": "2026-10-02T10:30:00+05:30",
"my_response": "…",
"notes_ready": true,
"notes_viewed": true,
"notes_writing": true,
"on_calendar": true,
"open_suggestions": 1,
"record_access": "host",
"recording_available": true,
"recording_enabled": true,
"scheduled_at": "2026-10-02T10:30:00+05:30",
"started_at": "2026-10-02T10:30:00+05:30",
"status": "…",
"summary_template": "…",
"title": "Pricing review",
"transcription_enabled": true,
"whiteboard_open": true
}Host only, and only before it starts. Distinct from /end, which stops
one that is running — cancelled and completed are different facts about
a meeting. The link people hold keeps working, and says it was cancelled.
/meetings/{meeting_id}/cancelWho hears it: everyone the host invited (invitations) — colleagues, with an
account or without, and guests the host invited. Google tells the colleagues
on the event as it clears it from their calendars, and Morse emails everyone
else, so each person hears it once (plan 002 D10). Not a guest who typed
their own address at the door: anyone holding the link can do that, with any
address, and emailing it the title would tell a link visitor what ADR-009
says they never learn. Anyone knocking is told through the lobby instead.
Email being down does not keep the meeting on: it is cancelled either way,
and not_emailed tells the host who still needs telling.
POST/meetings/{meeting_id}/cancel
curl -X POST "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60/cancel" \
-H "Authorization: Bearer $MORSE_TOKEN" \
-H "x-timezone: Asia/Kolkata"{
"emailed": "priya@neuralarc.ai",
"not_emailed": "priya@neuralarc.ai"
}Knowledge worth having open for this meeting.
/meetings/{meeting_id}/contextThe query is built here rather than taken from the caller, for two reasons:
the booking note is the host's alone, so the server has to decide whether it
goes in; and a route that embedded and searched arbitrary text would be an
open retrieval endpoint, which is a cost to anyone who asks and a way to
probe someone else's Knowledge one phrase at a time.
Authorised by the same predicate as the list (_IN_LIST): if the meeting is
already on your page, you may ask what relates to it. Results are scoped to
your own Knowledge by search itself, in SQL, so this cannot widen what you
can read.
Returns an empty list rather than an error whenever there is nothing honest
to say — no key, nothing embedded, a title that is only a date. The card
that reads this shows nothing in that case, which is the right answer.
GET/meetings/{meeting_id}/context
curl "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60/context" \
-H "Authorization: Bearer $MORSE_TOKEN"{
"notes": [
{
"doc_id": "3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60",
"excerpt": "…",
"folder_name": "Priya Shah",
"title": "Pricing review"
}
]
}End a meeting for everyone, which starts its notes being written.
/meetings/{meeting_id}/endHost only.
POST/meetings/{meeting_id}/end
curl -X POST "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60/end" \
-H "Authorization: Bearer $MORSE_TOKEN"An attendee removing a finished meeting from their own list. Nothing else changes: the meeting, its minutes and everyone else's list are as they were, and a direct link still opens it.
/meetings/{meeting_id}/hideNot for the host, who owns the meeting and deletes it instead — hiding it from the one list that matches on created_by would do nothing. Only ended meetings, because hiding an upcoming or live one would lose the way in. Idempotent: hiding twice keeps the first time.
POST/meetings/{meeting_id}/hide
curl -X POST "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60/hide" \
-H "Authorization: Bearer $MORSE_TOKEN"Undo a hide: the meeting is back in the caller's own list.
/meetings/{meeting_id}/hideIdempotent.
DELETE/meetings/{meeting_id}/hide
curl -X DELETE "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60/hide" \
-H "Authorization: Bearer $MORSE_TOKEN"Join a meeting: a LiveKit token to connect with, or waiting when it is locked and someone
in the room has to let you in.
/meetings/{meeting_id}/joinRefused once it has ended or been cancelled.
POST/meetings/{meeting_id}/join
curl -X POST "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60/join" \
-H "Authorization: Bearer $MORSE_TOKEN"{
"anyone_can_share": true,
"display_name": "Priya Shah",
"livekit_url": "https://onmorse.com/priya",
"meeting_role": "…",
"room_name": "Priya Shah",
"status": "…",
"token": "mp_YOUR_MORSE_TOKEN",
"whiteboard_open": true
}Covers leaving the lobby as well as the room.
/meetings/{meeting_id}/leaveWithout the waiting case
the host keeps seeing a knock from someone who has gone, and the waiting
badge counts a ghost.
POST/meetings/{meeting_id}/leave
curl -X POST "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60/leave" \
-H "Authorization: Bearer $MORSE_TOKEN"Who is in the room, for the pre-join screen: whether joining walks into a meeting already under way or an empty room.
/meetings/{meeting_id}/occupancyAnyone who may open the meeting may ask; the lobby already tells anyone waiting the same thing.
GET/meetings/{meeting_id}/occupancy
curl "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60/occupancy" \
-H "Authorization: Bearer $MORSE_TOKEN"{
"count": 3,
"names": [
"Priya Shah"
]
}Undo a delete.
/meetings/{meeting_id}/restoreHost only. Deleting only ever hid the meeting, so this puts it back for everyone as it was. Restoring one that isn't deleted is not an error.
POST/meetings/{meeting_id}/restore
curl -X POST "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60/restore" \
-H "Authorization: Bearer $MORSE_TOKEN"The caller opened this meeting's notes. Clears the unread mark in their own list and nobody else's.
/meetings/{meeting_id}/viewedGated on the same rule as the list — the host, or anyone with a participant row who was not shown the door — because a mark on a meeting that is not in your list is one nobody can see. 404 rather than 403, so a stranger learns nothing about whether it exists. Idempotent, and the first open is the one kept.
POST/meetings/{meeting_id}/viewed
curl -X POST "$MORSE/meetings/3f9c1a24-5e6f-4b31-9a77-1b2c3d4e5f60/viewed" \
-H "Authorization: Bearer $MORSE_TOKEN"