Section 4: Messages, Not Endpoints - API Design (with pushback)
Executive Summary: This section designs the contract for both features as messages on the meeting's existing WebSocket, not as REST endpoints. Reactions get one fire-and-forget message up and one coalesced frame down, with no acknowledgement and a one-second shelf life. Raise hand gets commands with client-generated opIds, server acks and rejects, versioned deltas, and snapshots. We then defend six choices under pushback: no REST, no acks for reactions, counts instead of clicks, server-assigned order, opId on an "idempotent" command, and the epoch field.
The interviewer caps the marker: "Walk me through your API." You start with the observation that shapes everything else.
You: "Every participant already has a persistent signaling connection open for the roster and mute state. Reactions and hands ride that socket as typed messages. The only REST endpoint is for meeting options, which change a few times per meeting at most."
The Message Catalog
Every message is a small JSON object whose msg field names it; the rest is its payload. JSON is fine for v1; a binary encoding later changes bytes, not semantics.
Client to server:
{ "msg": "reaction.send", "meetingId": "mtg-7f3a", "type": "applause", "count": 2, "clientTs": 1793293923250 }
{ "msg": "hand.raise", "meetingId": "mtg-7f3a", "opId": "9c1e…" }
{ "msg": "hand.lower", "meetingId": "mtg-7f3a", "opId": "c41d…", "targetParticipantId": "p-sam" }
{ "msg": "hand.lowerAll", "meetingId": "mtg-7f3a", "opId": "e07a…" }
{ "msg": "meeting.sync", "meetingId": "mtg-7f3a", "haveVer": 3 }
targetParticipantId is optional: absent means "lower my own hand." haveVer lets the owner answer "you're current" when it matches; otherwise the reply is a full hand.snapshot.
Server to client:
{ "msg": "reaction.burst", "meetingId": "mtg-7f3a", "tick": 5120,
"counts": { "like": 0, "heart": 3, "applause": 212, "laugh": 0, "surprised": 1 },
"senders": ["p-maya", "p-0193", "…up to 10"] }
{ "msg": "hand.ack", "opId": "9c1e…", "ver": 4 }
{ "msg": "hand.reject", "opId": "c41d…", "reason": "forbidden" }
{ "msg": "hand.delta", "meetingId": "mtg-7f3a", "epoch": 3, "ver": 4,
"changes": [{ "op": "raised", "participantId": "p-maya", "order": 4, "by": "p-maya" }] }
{ "msg": "hand.snapshot", "meetingId": "mtg-7f3a", "epoch": 3, "ver": 4,
"hands": [{ "participantId": "p-ana", "order": 1, "raisedAt": "2026-10-29T17:40:02Z" },
{ "participantId": "p-raj", "order": 2, "raisedAt": "2026-10-29T17:40:03Z" },
{ "participantId": "p-li", "order": 3, "raisedAt": "2026-10-29T17:40:03Z" },
{ "participantId": "p-maya", "order": 4, "raisedAt": "2026-10-29T17:40:04Z" }] }
reason is forbidden, disabled or unavailable; only unavailable is worth retrying.
The one REST call: PATCH /meetings/{id}/options {reactionsEnabled, handRaiseEnabled}. It is an organizer's settings change, rare, and it belongs with the rest of meeting configuration, where HTTP authorization and audit already live. The meeting owner learns about the change from the config service and rejects those commands with disabled.
| Message | Direction | Acked? | Droppable? | Size |
|---|---|---|---|---|
reaction.send |
up | No | Yes | about 100 B |
reaction.burst |
down | No | Yes, after 1 s | at most 300 B |
hand.raise / lower / lowerAll |
up | Yes, hand.ack or hand.reject |
No, retried with the same opId |
small |
hand.delta |
down | No, gaps are detected by ver |
No | small |
hand.snapshot |
down | No | No | under 1 KB typical, 64 KB worst |
The 300 B ceiling on reaction.burst comes straight from its shape: five counts, at most ten sender IDs at 16 B each, and a header. Notice what is missing from every upstream message: a sender. The server stamps participantId from the authenticated session at the gateway. A client cannot clap as someone else or lower another person's hand unless its session carries the organizer or presenter role. Identity is a property of the connection, not a field the client fills in.
Here is Maya's clap and Maya's hand on the same socket, side by side:
The top half has no ack, no retry and no disk: Maya's client draws her clap locally, then receives the same room-wide frame as everyone else and subtracts her own clicks. The bottom half has an ack and a disk. That asymmetry is the module's one-line idea expressed as a protocol.
Quick check before moving on:
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson