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 · Route | Purpose |
|---|---|
GET / | List meetings (participants + RSVP status) |
GET /{meetingId} | Meeting detail |
POST / | Create a meeting — materializes agenda items and sends RSVP invitation emails |
PUT /{meetingId}/minutes | Save minutes body directly |
GET /{meetingId}/agenda-items | Agenda items (title, memo, notes, status, outcome, recording) |
POST /{meetingId}/agenda-items | Add 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}/open | Take up the item — voting opens here |
POST /{meetingId}/agenda-items/{index}/close | Close the item (voting shuts) |
PUT /{meetingId}/agenda-items/{index}/recording | Attach an uploaded audio file to the item |
POST /{meetingId}/participants/{participantId}/attendance | Mark a participant present or absent — this is what makes quorum |
GET /{meetingId}/agenda-votes | Every vote on this meeting |
POST /{meetingId}/agenda-votes | Cast your vote on one item (personal seal required) |
GET /{meetingId}/close-preview | What each item would be recorded as, before you close |
POST /{meetingId}/minutes/compile | Compose all items into one set of minutes |
POST /{meetingId}/complete | Close the meeting — fixes outcomes, files the minutes, optionally carries items forward |
POST /{meetingId}/cancel | Call 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) |
Who can do what
Section titled “Who can do what”RSVP is a statement of intent, not an access grant. Two different roles matter:
| Actor | Can |
|---|---|
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.
Agenda items
Section titled “Agenda items”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.
Status | Meaning |
|---|---|
Pending | Not yet taken up — voting is refused (AGENDA_NOT_OPENED) |
Open | Taken up; voting accepted |
Closed | Voting 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.
Adding, fixing, and dropping items
Section titled “Adding, fixing, and dropping items”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_VOTESon 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.
Board votes
Section titled “Board votes”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 viaPUT /api/org/{slug}/board/chairman(personal seal required).
Attendance is what makes quorum
Section titled “Attendance is what makes quorum”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-previewshows 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.
One director, one vote
Section titled “One director, one vote”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.
| Field | Notes |
|---|---|
AgendaIndex | int |
Decision | Assent · Dissent · Abstain (case-insensitive) |
Reason | optional; the record of why for a dissent or abstention |
SealAssertionJson, SealEmail | personal 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.
Outcomes
Section titled “Outcomes”close-preview and complete run the same judge, so what the confirmation dialog shows is
what gets recorded:
Outcome | When |
|---|---|
NoQuorum | Fewer than a majority of directors checked in — evaluated first |
NotVoted | Quorum met, but a director who attended has not voted (or the item was never taken up) |
Passed | Everyone who attended voted; assent is a majority of those present |
Rejected | Everyone 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.
Closing a meeting
Section titled “Closing a meeting”POST /{meetingId}/complete — irreversible:
- Each agenda item’s outcome is fixed and the item is closed.
- Minutes are recomposed (so a last note or vote is not left out) and stored.
- A copy is filed in the organization’s Files under the meeting’s folder.
- Status becomes
Completed; the meeting leaves the schedule.
| Body field | Notes |
|---|---|
CreateFollowUp | true 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.
Cancelling a meeting
Section titled “Cancelling a meeting”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.
Files: one folder per meeting
Section titled “Files: one folder per meeting”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.
Plan and add-on
Section titled “Plan and add-on”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.
Seal assurance is not a legal gate
Section titled “Seal assurance is not a legal gate”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.
Meeting types
Section titled “Meeting types”Type | Value | Status |
|---|---|---|
TeamMeeting | 0 | Available |
Client | 1 | Available |
BoardMeeting | 10 | Available — board, votes, and outcomes as above |
Shareholder | 11 | On development — share-weighted voting belongs here, not on the board |
Executive | 12 | On development |
Create body (POST /)
Section titled “Create body (POST /)”| Field | Type | Notes |
|---|---|---|
Title | string | required |
Type | int | see meeting types above |
Mode | int | 0 = Offline, 1 = Online (LiveKit provider) |
ScheduledAt | UTC datetime | |
DurationMinutes | int | |
Location | string? | optional |
Agenda | string? | newline-separated; one agenda item per line. Required for BoardMeeting |
ParticipantConnectedIds | Guid[] | member participants |
ExternalParticipants | [{ Email }] | external invitees (email RSVP) |