Skip to content

Meetings

Base path: /api/org/{slug}/meetings. Creating a meeting requires the organization’s Master Seal (ORG_SEAL_REQUIRED otherwise) — meeting records are sealed.

A meeting runs item by item: the chair takes up an agenda item, voting opens on that item, discussion and audio attach to it, and closing the meeting composes everything into minutes that are filed in the organization’s Files.

Method · RoutePurpose
GET /List meetings (participants + RSVP status)
GET /{meetingId}Meeting detail
POST /Create a meeting — materializes agenda items and sends RSVP invitation emails
PUT /{meetingId}/minutesSave minutes body directly
GET /{meetingId}/agenda-itemsAgenda items (title, memo, notes, status, outcome, recording)
POST /{meetingId}/agenda-itemsAdd an item raised during the meeting — numbered last
PUT /{meetingId}/agenda-items/{index}Save an item’s pre-meeting memo / discussion notes, or fix its wording
DELETE /{meetingId}/agenda-items/{index}Drop an item that has no votes (soft)
POST /{meetingId}/agenda-items/{index}/openTake up the item — voting opens here
POST /{meetingId}/agenda-items/{index}/closeClose the item (voting shuts)
PUT /{meetingId}/agenda-items/{index}/recordingAttach an uploaded audio file to the item
POST /{meetingId}/participants/{participantId}/attendanceMark a participant present or absent — this is what makes quorum
GET /{meetingId}/agenda-votesEvery vote on this meeting
POST /{meetingId}/agenda-votesCast your vote on one item (personal seal required)
GET /{meetingId}/close-previewWhat each item would be recorded as, before you close
POST /{meetingId}/minutes/compileCompose all items into one set of minutes
POST /{meetingId}/completeClose the meeting — fixes outcomes, files the minutes, optionally carries items forward
POST /{meetingId}/cancelCall off a meeting that never happened (reason required, no votes)
DELETE /{meetingId}Delete a closed meeting (soft)
GET /api/meetings/rsvp/{participantId}/{response}Signed RSVP link from the invitation email (accepted / declined / tentative)

RSVP is a statement of intent, not an access grant. Two different roles matter:

ActorCan
Organizer (ParticipantRole.Organizer)Everything below, plus the irreversible actions: compile minutes, close-preview, close the meeting, delete it
Chairman (the board’s LeadConnectedId)Take up / close items, save memo and notes, attach recordings, mark attendance
Director (ORG_DIRECTOR)Reach the meeting screens and cast their own vote

Directors need view.business, org.meeting.read, and org.meeting.list. These ship in the ORG_DIRECTOR preset — a director who cannot open the meeting screen cannot vote, so the role carries the access rather than depending on a grant.

Writing minutes and closing the meeting are deliberately split: the chairman may write, but closing is irreversible (minutes are fixed, a copy is filed, the meeting leaves the schedule), so it belongs to whoever convened it. The server enforces this, not just the UI.

POST / always gives the organizer a participant row, whether or not they put themselves on the list — otherwise nobody would hold the organizer-only actions and the meeting could never be closed. That row is not checked in: convening a meeting and attending it are different facts.

POST / splits Agenda on newlines (empty lines dropped, trimmed) and creates one row per line, numbered from 0. That number is what votes point at.

StatusMeaning
PendingNot yet taken up — voting is refused (AGENDA_NOT_OPENED)
OpenTaken up; voting accepted
ClosedVoting shut (AGENDA_CLOSED)

An item carries a pre-meeting Memo (background to read beforehand) and Notes (what was said), saved independently — sending only one of them leaves the other untouched.

The agenda is not frozen at creation. An item raised during the meeting goes in with POST /{meetingId}/agenda-items (body { "title": "...", "memo": "..." }), PUT .../{index} also takes a Title to fix wording, and DELETE .../{index} drops one.

Two rules hold this together, and both exist because votes point at the number:

  • Numbers never move. A new item is numbered after the highest number the meeting has ever used — deleted ones included, so a number is never reused. Deleting leaves a gap in the sequence, and that gap is correct: renumbering would silently move votes onto a different item. There is no reorder endpoint for the same reason.
  • An item with votes is frozen — 400 AGENDA_HAS_VOTES on a wording change or a delete. Every vote stores the wording it was cast on (this is what supports the “who dissented, and why” record), so rewriting the item afterwards would break what a director actually agreed to. Take up a new item instead.

Adding, rewording, or dropping an item rebuilds the meeting’s Agenda summary string and emits business.meeting.updated. Item rows remain the source of truth for numbering.

Type = BoardMeeting requires the organization’s Board of Directors:

  • BOARD_REQUIRED — no board, or no directors. Seat directors by inviting members with the Director role (Members → Director invitations).
  • BOARD_CHAIRMAN_REQUIRED — no chairman. Designate one via PUT /api/org/{slug}/board/chairman (personal seal required).

Quorum counts the directors who attended, not the ones who were invited. Being on the participant list is an invitation; CheckedInAt is the fact.

POST /api/org/{slug}/meetings/{meetingId}/participants/{participantId}/attendance
{ "present": true }

The organizer or the chairman marks it, and it is reversible — send present: false to undo a mistake. No seal is required for that reason; the record is carried by business.meeting.attendance_marked on the append-only audit chain, which also distinguishes a mark made by hand from one made automatically.

Casting a vote marks the voter present automatically. Stamping a vote proves the director was there, so a chairman who forgets to check someone in does not lose that director from the count. What check-in adds is the director who attended and did not vote — without it, quorum could only ever be proven by voting.

Two consequences worth planning for:

  • A meeting where nobody is checked in and nobody has voted has no quorum, whatever the invite list says. close-preview shows the count before anything is recorded.
  • Only the votes of currently seated, present directors are counted. A vote left behind by a director who has since been removed from the board no longer inflates the tally.

Board resolutions count people, not shares. Quorum is a majority of the total number of directors, and a resolution carries on a majority of the directors present.

A director must also be a participant of this meeting to vote (NOT_A_PARTICIPANT, 403) — sitting on the board is not by itself a seat at a meeting they were not called to.

Directors do not vote by proxy: POST /agenda-votes takes no voter field — the vote is bound to the session’s ConnectedId. It also takes no agenda text; the server reads the wording from the item row so the voter cannot author what the record says they voted on.

FieldNotes
AgendaIndexint
DecisionAssent · Dissent · Abstain (case-insensitive)
Reasonoptional; the record of why for a dissent or abstention
SealAssertionJson, SealEmailpersonal seal (passkey step-up) — required

The seal is bound to meeting + item + choice (StepUpPurpose.BoardVoteSubject), so a seal taken for item 1 cannot stamp item 2, and a seal taken to dissent cannot record an assent.

Abstain is a distinct value on purpose: a director who is present is treated as having assented unless a dissent or abstention is on the record, so “no button for abstain” would silently convert an abstention into a yes. Tallies show the not-yet-voted count for the same reason.

Re-voting overwrites the row (one row per item per director). The change itself is not lost — every cast emits business.meeting.agenda_voted with a chg flag onto the append-only audit chain.

close-preview and complete run the same judge, so what the confirmation dialog shows is what gets recorded:

OutcomeWhen
NoQuorumFewer than a majority of directors checked in — evaluated first
NotVotedQuorum met, but a director who attended has not voted (or the item was never taken up)
PassedEveryone who attended voted; assent is a majority of those present
RejectedEveryone who attended voted; assent is not a majority

NoQuorum is not Rejected. A resolution that never became capable of being taken is a different fact from one that was voted down, and the minutes say so.

POST /{meetingId}/complete — irreversible:

  1. Each agenda item’s outcome is fixed and the item is closed.
  2. Minutes are recomposed (so a last note or vote is not left out) and stored.
  3. A copy is filed in the organization’s Files under the meeting’s folder.
  4. Status becomes Completed; the meeting leaves the schedule.
Body fieldNotes
CreateFollowUptrue copies items that reached no decision (NoQuorum / NotVoted) into a new meeting — same participants and type, scheduled a week out, with CarriedFromMeetingId recording where each came from

Items are copied, not moved: the original rows stay as the record of “no quorum”, because the minutes already say that.

The follow-up is created through the same command as any other meeting, so it passes the same doors: the plan gate, the board add-on quota, the board and chairman requirements — and it emits business.meeting.created and sends invitations like any meeting. It is a new meeting, and it counts as one.

The response reports minutesFileId and, if filing failed, minutesFileError — closing is already committed at that point, so the screen tells the truth rather than implying the close failed. If the follow-up could not be created, followUpError names why (the add-on key, or the board gate sentinel) instead of failing the close.

POST /{meetingId}/cancel — body { "reason": "..." }, reason required (REASON_REQUIRED). For a meeting that never took place: no outcome is recorded on the items and no minutes are filed. Status becomes Cancelled, which is what makes the meeting deletable.

A meeting with votes cannot be cancelled — 400 MEETING_HAS_VOTES. A vote means the meeting actually sat and decided something; that is a close, not a cancellation. Without this rule, cancel followed by delete would be a way to make a real vote disappear from the schedule.

Cancelling emits business.meeting.cancelled.

DELETE /{meetingId} only accepts a Completed or Cancelled meeting (ONLY_COMPLETED_CAN_BE_DELETED). It is a soft delete: the filed minutes and the audit chain remain.

Meeting records live in the organization’s Files under scope meeting:{meetingId} — a folder per meeting appears on its own, with no folder-creating step. Storage region, quota, sharing, audit, and retention rules are inherited from the Files axis.

Upload with POST /api/org/{slug}/files?meetingId={id} (mirrors ?appGroupId=).

Audio is kept per agenda item, not as one file for the meeting: a single long recording cannot answer “what was said on item 3”. Recordings stop themselves just under the upload limit and save, rather than failing the upload after the meeting is over.

Meetings require a paid plan (Pro or higher) — UPGRADE_REQUIRED otherwise. Board meetings sit behind the same door, with the corporation / SPV entity gate still in front of it: an entity without a board has no board meetings regardless of plan.

The first board meeting is free for each organization. From the second one, POST / answers 402 with ADDON_REQUIRED and the addon.board_meeting key ($99/mo) so the caller can offer the purchase in place rather than a dead end.

{
"error": "ADDON_REQUIRED",
"addon": "addon.board_meeting",
"freePerOrg": 1,
"everCreated": 1,
"priceUsd": 99.00
}

The free count is per organization for its lifetime, not per month, and deleting a board meeting does not give it back — otherwise create-and-delete would reopen it indefinitely.

Votes are confirmed with each director’s personal seal (passkey), which satisfies the statutory signature requirement for board minutes. Hardware-backed signing (Business plan, Defense tier) raises tamper resistance, not legal validity — it is never required to hold a valid board meeting, and nothing about the flow is blocked without it.

TypeValueStatus
TeamMeeting0Available
Client1Available
BoardMeeting10Available — board, votes, and outcomes as above
Shareholder11On development — share-weighted voting belongs here, not on the board
Executive12On development
FieldTypeNotes
Titlestringrequired
Typeintsee meeting types above
Modeint0 = Offline, 1 = Online (LiveKit provider)
ScheduledAtUTC datetime
DurationMinutesint
Locationstring?optional
Agendastring?newline-separated; one agenda item per line. Required for BoardMeeting
ParticipantConnectedIdsGuid[]member participants
ExternalParticipants[{ Email }]external invitees (email RSVP)