Board

45 operations. Every schema and example on this page is generated from the platform contract.

Recent board-wide activity feed (board_activity), newest first, capped.

Parameters

limitintegerqueryoptional

How many recent events to return (default 100, max 200).

Response, 200

activityarray of objectrequired
6 child fields
idnumberrequired
ticket_idstringrequired
actorstringrequired
verbstringrequired
detailstring, nullablerequired
created_atstringrequired
curl -G "https://www.membber.com/api/v1/board/activity" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "activity": [
    {
      "id": 1,
      "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
      "actor": "<actor>",
      "verb": "<verb>",
      "detail": "<detail>",
      "created_at": "<created_at>"
    }
  ]
}

The pick-next ranking: eligible unclaimed Todo items a thread could take.

Parameters

limitintegerqueryoptional

How many candidates to return (default 12, max 50).

Response, 200

candidatesarray of objectrequired
14 child fields
idstringrequired
titlestring, nullablerequired
typestringrequired
sectionstring, nullablerequired
epic_idstring, nullablerequired
priostringrequired
founder_ordnumber, nullablerequired
founder_rankedbooleanrequired
tierstringrequired

'founder-binding' when the founder has ranked it, else 'judgement'.

unblocks_countnumberrequired
age_daysnumber, nullablerequired
adjacent_to_inflightbooleanrequired
branchstring, nullablerequired
pr_numbernumber, nullablerequired
curl -G "https://www.membber.com/api/v1/board/candidates" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "candidates": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "title": "Morning class",
      "type": "<type>",
      "section": "<section>",
      "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
      "prio": "<prio>",
      "founder_ord": 1,
      "founder_ranked": true,
      "tier": "<tier>",
      "unblocks_count": 1,
      "age_days": 1,
      "adjacent_to_inflight": true,
      "branch": "<branch>",
      "pr_number": 1
    }
  ]
}

Legacy: pick a ticket up. Kept only for the installed HQ app; use assign + move instead.

Claim was removed from the product (R12). This route survives ONLY because an installed binary calls it, and it now delegates to the assign path: the thread is registered, given the ticket, and the ticket moves to Doing. Still returns 409 when another thread already holds it, exactly as before.

Request body

idstringrequired

The ticket to pick up.

sessionstringrequired

The thread picking it up.

assigneestringrequired

Who is picking it up.

Response, 200

ticketobjectrequired
50 child fields
idstringrequired
sourcestringrequired
pr_numbernumber, nullablerequired
branchstring, nullablerequired
sectionstring, nullablerequired
colstring, nullablerequired

Resolved column: the founder override (founder_col) if set, else the synced col.

founder_colstring, nullablerequired

The founder column override, or null when the item is not founder-pinned.

pr_statestring, nullablerequired
typestringrequired

Resolved type: type_override if set, else the synced type.

priostringrequired

Resolved priority: prio_override if set, else the synced prio.

titlestring, nullablerequired

Resolved title: display_title ?? title ?? raw_title.

raw_titlestring, nullablerequired
summarystring, nullablerequired

The plain-terms "In short" TL;DR every ticket opens with.

after_donestring, nullablerequired

Founder-action rows: "What happens after you tick it", the post-done "Next:" card.

whystring, nullablerequired
what_it_doesstring, nullablerequired
user_storystring, nullablerequired
epic_idstring, nullablerequired
when_tsstring, nullablerequired

ISO date (YYYY-MM-DD) the work landed / was raised.

content_sourcestring, nullablerequired
synced_atstringrequired

ISO timestamp of the last sync write.

assigneestring, nullablerequired
claimed_atstring, nullablerequired
session_idstring, nullablerequired
depends_onarray of stringrequired

R16: ticket ids this one is waiting on. Empty = not blocked.

labelsarray of stringrequired
notesstring, nullablerequired
founder_ordnumber, nullablerequired
created_tsstring, nullablerequired

The time dimension: when the item was born (PR createdAt / FA added date). Null when the source has no date.

updated_tsstring, nullablerequired

When the item last moved at its source (PR updatedAt). Null when unknown.

initiativestring, nullablerequired

The ONE initiative/vertical this ticket serves: gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform. 'platform' is the honest cross-cutting none. Null = never classified. (Read kept permissive: the vocabulary may grow without a migration.)

merchantstring, nullablerequired

Canonical store slug when the ticket is about ONE real store (busy-beans, tc60, flour-bloom, larkfield, …). Null = no single store / never classified.

addonsarray of stringrequired

Add-on slugs from the real addons catalog (order_collect, bookings, gym_management, own_app, retention_engine) plus family-level 'moments'. Null = never classified; [] = classified as none, the distinction is deliberate and kept on the wire.

parent_idstring, nullablerequired

R17: the ticket this is a sub-task of, or null when it stands alone.

human_contextstring, nullablerequired

R14: the SHORT plain-English line, AS AUTHORED. Null when nobody has written one. getBoardTicket returns a derived fallback in its own top-level human_context field; this column stays honest about whether a human actually wrote it.

assignee_kindenumrequired

'human' with an assignee = a person owns it. 'ai' with a session_id = THAT thread owns it. 'ai' with no session_id = the pool, any thread may pick it up. null = unassigned.

humanai
reopened_atstring, nullablerequired

R9: when it last came back out of Done, or null if it never has.

reopen_countintegerrequired

R9: how many times it has come back out of Done. 0 for most tickets.

capture_textstring, nullablerequired

Founder quick capture (18 Aug 2026): his VERBATIM words at capture time (source 'founder-capture'). Kept forever, even after triage fills the brief, his words are the record. Null on every other source.

subtask_idsarray of stringrequired

R17: ids of the tickets whose parent_id is this one. Computed on read, never written.

children_doneintegerrequired

Honest per-ticket progress (19 Aug 2026): how many of subtask_ids sit in a RESOLVED column of 'done', the DONE half of the card's "3 of 8". The TOTAL is subtask_ids.length, so both halves have one source and cannot disagree. 0 when there are no children. Never a percentage.

created_by_founderbooleanrequired

True when the FOUNDER filed this row himself, across all three intake paths: quick capture (source='founder-capture'), the ⊕ New-ticket sheet (source='manual' AND content_source='founder'), or a bug he raised that was later filed against a board_inbox row with asking_session_id NULL. Computed by the one server-side predicate (isFounderCreated in src/lib/board/resolve.ts), never re-derived from the id prefix, which is wrong on the live board.

founder_ownedbooleanrequired

True when the row is HIS: everything created_by_founder covers, PLUS his own errand list (source='founder-action', the FA items). Strictly wider than created_by_founder and never narrower. Two fields because the questions differ, provenance decides whether a trust-loop push fires (only for what he actually filed), while the board's "Yours" view decides what he sees. Computed by isFounderOwned in src/lib/board/resolve.ts.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

Computed, never written. working_now = a LIVE session (silent < 45 min, the board_liveness_window) holds it by session_id OR assignee=label OR branch=branch, the ONLY state a screen may present as being worked. started_nobody_on_it = in a started column and NO session has ever held it. abandoned_was_held = a session did hold it and that thread is finished or long silent. not_applicable = not in a started column at all (To-do, Done, Blocked, Triage have no liveness question to answer).

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
worked_bystring, nullablerequired

The live developer's name when work_state is working_now ("Tomas Ashworth"), else null. The ONLY field a card may use to claim somebody is working, there is no client-side fallback, by design.

was_worked_bystring, nullablerequired

The name of the developer who HAD it, when nobody has it now. Null while someone is on it, and null when nobody ever was, so an abandoned card can say "Hugh Draper stopped" instead of going anonymous.

went_quiet_atstring, nullablerequired

When that developer was last seen, so a card can say "quiet since 09:14".

released_atstring, nullablerequired

When this ticket was last taken off a dead thread and handed back. claimed_at is PRESERVED alongside it, so "started" and "given back" are both readable.

ai_contextobjectoptional

R15, DETAIL ONLY. Present on getBoardTicket, ABSENT on a list row and on a mutation echo: rendering it for 123 rows would be megabytes on every board refresh. A client keeps the copy it loaded when a fresher row arrives without one, rather than blanking the brief mid-session.

3 child fields
markdownstringrequired

The whole brief as one paste-and-go document. This is what an agent is handed.

sectionsarray of objectrequired
4 child fields
keystringrequired

Stable and machine-readable: what_needs_doing, at_a_glance, pr, history, ...

titlestringrequired
bodystring, nullablerequired
itemsarray of stringrequired

List-shaped sections (dependencies, sub-tasks, history) come back as items.

generated_atstringrequired

When the server composed it, so a stale brief is visible as stale.

curl -X POST "https://www.membber.com/api/v1/board/claim" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "session": "<session>",
    "assignee": "<assignee>"
  }'
Response, 200
{
  "ticket": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "source": "<source>",
    "pr_number": 1,
    "branch": "<branch>",
    "section": "<section>",
    "col": "<col>",
    "founder_col": "<founder_col>",
    "pr_state": "<pr_state>",
    "type": "<type>",
    "prio": "<prio>",
    "title": "Morning class",
    "raw_title": "<raw_title>",
    "summary": "<summary>",
    "after_done": "<after_done>",
    "why": "<why>",
    "what_it_does": "<what_it_does>",
    "user_story": "<user_story>",
    "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
    "when_ts": "<when_ts>",
    "content_source": "<content_source>",
    "synced_at": "<synced_at>",
    "assignee": "<assignee>",
    "claimed_at": "<claimed_at>",
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "depends_on": [
      "<depends_on>"
    ],
    "labels": [
      "<label>"
    ],
    "notes": "Added at the front desk",
    "founder_ord": 1,
    "created_ts": "<created_ts>",
    "updated_ts": "<updated_ts>",
    "initiative": "<initiative>",
    "merchant": "<merchant>",
    "addons": [
      "<addon>"
    ],
    "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
    "human_context": "<human_context>",
    "assignee_kind": "human",
    "reopened_at": "<reopened_at>",
    "reopen_count": 1,
    "capture_text": "<capture_text>",
    "subtask_ids": [
      "e8d74bd5-0000-4000-8000-d0c5000000e8"
    ],
    "children_done": -9007199254740991,
    "created_by_founder": true,
    "founder_owned": true,
    "lifecycle": "awaiting_triage",
    "work_state": "working_now",
    "worked_by": "<worked_by>",
    "was_worked_by": "<was_worked_by>",
    "went_quiet_at": "<went_quiet_at>",
    "released_at": "<released_at>",
    "ai_context": {
      "markdown": "<markdown>",
      "sections": [
        {
          "key": "<key>",
          "title": "Morning class",
          "body": "<body>",
          "items": [
            "<item>"
          ]
        }
      ],
      "generated_at": "<generated_at>"
    }
  }
}

File an on-device hang as a deduped board ticket, never as an inbox item.

The device watchdog and MetricKit report here. Repeats FOLD: the ninth hang on the same screen, reporter, device class and build appends an occurrence comment to one ticket and refreshes its count and worst duration, rather than creating a ninth row. Idempotent on source_key, so a retry cannot double-count. Simulator reports (device_class is a host arch) are filed low and labelled honestly rather than hidden, because they are still real observations.

Request body

screenstringrequired

The frontmost screen when the main thread stalled, e.g. "Board". Part of the dedupe key.

sourceenumrequired

Which of the two reporters saw it: the in-process watchdog, or Apple MetricKit.

watchdogmetrickit
secondsnumberrequired

How long the main thread was blocked. The worst value across occurrences rides in the title.

device_classstringrequired

uname().machine, a real model ("iPhone18,2") on a device, the HOST ARCH ("arm64") on a simulator. This is a FIELD rather than something regexed out of a rendered string, because it is what tells a real defect from the Apple sim host-bridge wedge. 10 of the first 12 live hang reports were arm64.

app_buildstringrequired

Short version + build, e.g. "1.0(1)". Part of the dedupe key.

environmentstringoptional

The full rendered environment line, kept verbatim.

source_keystringrequired

This ONE hang's idempotency key, derived from its own timestamp. Passed straight through to the occurrence comment, so a delivery retry can never count the same hang twice.

detailstringoptional

The full report body, call stack where MetricKit supplied one, honestly labelled.

Response, 200

ticket_idstringrequired

The board ticket this occurrence was folded onto.

occurrencesintegerrequired

How many times this exact hang has now been seen.

worst_secondsnumberrequired

The longest occurrence in this bucket.

simulatorbooleanrequired

True when the report came from a simulator, filed low, and labelled as such.

createdbooleanrequired

True when this occurrence created the ticket rather than appending to it.

curl -X POST "https://www.membber.com/api/v1/board/diagnostics/hang" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "screen": "<screen>",
    "source": "watchdog",
    "seconds": 0,
    "device_class": "<device_class>",
    "app_build": "<app_build>",
    "source_key": "<source_key>"
  }'
Response, 200
{
  "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
  "occurrences": -9007199254740991,
  "worst_seconds": 1,
  "simulator": true,
  "created": true
}

Is the fleet running or halted on a usage limit, plus the queue-state signal.

Reads the board_fleet_status singleton (written only by the Mac launcher from an observed, captured limit message) plus a computed queue signal: how many ranked tickets wait and whether any live developer is free. The app renders the halt banner from this; realtime `fleet_changed` broadcasts on the 'board' topic signal when to refetch.

Response, 200

fleetobjectrequired
7 child fields
statusenumrequired

running = work can flow; halted_limit = the Mac CLI is refusing work on a usage limit.

runninghalted_limit
messagestring, nullablerequired

The verbatim captured limit message, or null while running.

reset_atstring, nullablerequired

Parsed from the real "resets …" clause; null when the message states no reset time.

observed_atstringrequired

When the launcher last looked at reality (bumped on every recheck while halted).

sourcestring, nullablerequired

What observed it: probe | spawn_probe | transcript | heartbeat | wake.

wake_requested_atstring, nullablerequired

The founder's un-consumed Wake-now tap, if one is pending.

updated_atstringrequired
queueobjectrequired
4 child fields
waitingintegerrequired

Eligible unclaimed Todo tickets (board_next_candidates), in founder order.

top_titlestring, nullablerequired

The title at the top of the queue, or null when it is empty.

live_developersintegerrequired

AI sessions heard from inside the live window.

idle_developersintegerrequired

Of those, how many hold no in-flight work (their loop will absorb new tickets).

focusobjectrequired

The active fleet focus, or null when none is pointed.

7 child fields
areastring, nullablerequired

The pointed area slug (board_label_vocabulary), or null for a merchant focus.

merchantstring, nullablerequired

The pointed store slug, or null for an area focus.

labelstring, nullablerequired

The display name at point time ("Gym"), what banners and prompts say.

sectionsarray of stringrequired

The expanded predicate: sections half of the OR-union.

initiativesarray of stringrequired

The expanded predicate: initiatives half.

addonsarray of stringrequired

The expanded predicate: add-ons half.

pointed_atstringrequired

When the founder pointed it.

targetobjectrequired

The target dial: how many developers the Mac runs by itself, and why it currently is not.

15 child fields
target_sizeintegerrequired

How many developers to run. 0 = nothing starts itself.

stopped_atstring, nullablerequired

When the founder last hit Stop; null = never, or restarted since.

stop_reasonstring, nullablerequired

His words for the stop, if he gave any.

headroom_maxinteger, nullablerequired

The honest live ceiling, launcher-written and pre-clamped to its hard cap. Null until the launcher has looked.

headroom_reasonstring, nullablerequired

What binds the ceiling: 'disk' (free space) | 'cap' (the most this Mac runs at once).

disk_free_gbnumber, nullablerequired

Free space on the Developer volume at the last decision, the reason behind the ceiling.

headroom_observed_atstring, nullablerequired

When the launcher last measured the ceiling.

last_refusalstring, nullablerequired

Why nothing started on the last tick that wanted to: halted|stopped|off|disk|at_target|no_work|load|memory|hard_cap|breaker|backoff|rate_cap|ramp. The app owns the human words.

last_refusal_atstring, nullablerequired

When that refusal was last recorded. Advances on every refusing tick even when the reason is unchanged, so a stale card is distinguishable from a dead daemon.

last_refusal_detailstring, nullablerequired

The refusal SENTENCE beside the code, carrying the numbers only the Mac can see ("8.4 GB free leaves no room for another"). Evidence, not copy: the app still owns the friendly words for the code.

consecutive_failuresintegerrequired

Auto-launch failures since a spawned session last said hello. At 3 the circuit breaker latches and only ONE probationary developer is tried per escalating gap (30m/1h/2h/4h), so a wall is waited out rather than hammered.

last_failure_atstring, nullablerequired

When the last auto-launch failure was recorded, read back by the launcher at startup so the breaker survives a daemon restart.

last_failure_detailstring, nullablerequired

What actually failed, in a sentence, so a stopped fleet explains itself without anyone opening a log.

last_auto_spawn_atstring, nullablerequired

When the launcher last started one by itself.

updated_atstringrequired
curl -G "https://www.membber.com/api/v1/board/fleet" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "fleet": {
    "status": "running",
    "message": "Added at the front desk",
    "reset_at": "<reset_at>",
    "observed_at": "<observed_at>",
    "source": "<source>",
    "wake_requested_at": "<wake_requested_at>",
    "updated_at": "<updated_at>"
  },
  "queue": {
    "waiting": -9007199254740991,
    "top_title": "<top_title>",
    "live_developers": -9007199254740991,
    "idle_developers": -9007199254740991
  },
  "focus": {
    "area": "<area>",
    "merchant": "<merchant>",
    "label": "<label>",
    "sections": [
      "<section>"
    ],
    "initiatives": [
      "<initiative>"
    ],
    "addons": [
      "<addon>"
    ],
    "pointed_at": "<pointed_at>"
  },
  "target": {
    "target_size": -9007199254740991,
    "stopped_at": "<stopped_at>",
    "stop_reason": "Added at the front desk",
    "headroom_max": -9007199254740991,
    "headroom_reason": "Added at the front desk",
    "disk_free_gb": 1,
    "headroom_observed_at": "<headroom_observed_at>",
    "last_refusal": "<last_refusal>",
    "last_refusal_at": "<last_refusal_at>",
    "last_refusal_detail": "<last_refusal_detail>",
    "consecutive_failures": -9007199254740991,
    "last_failure_at": "<last_failure_at>",
    "last_failure_detail": "<last_failure_detail>",
    "last_auto_spawn_at": "<last_auto_spawn_at>",
    "updated_at": "<updated_at>"
  }
}

Release the fleet focus, the ranking returns to the standing order.

One tap undoes the point: stamps released_at on the singleton, writes the fleet:focus 'released' activity row, and board_next_candidates immediately reverts to the standing founder ranking. Idempotent: releasing when nothing is pointed is a truthful no-op.

Response, 200

releasedbooleanrequired

true = an active focus was released; false = there was none (a no-op, not an error).

curl -X DELETE "https://www.membber.com/api/v1/board/fleet/focus" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"
Response, 200
{
  "released": true
}

Point the fleet: every developer serves this area (or merchant) first until released.

The founder's deliberate focus directive, NEVER fired automatically. Writes the board_fleet_focus singleton (area/merchant + display label + the predicate expanded from board_label_vocabulary.area_terms), writes a fleet:focus activity row, and board_next_candidates puts matching tickets FIRST while the row is active, the kit worker loop, server-side next and the queue whisper all inherit it through that one RPC. Re-pointing replaces the focus (idempotent for the same target). It never times out server-side; DELETE releases it.

Request body

areastringoptional

An area slug from board_label_vocabulary (kind=area, non-complement).

merchantstringoptional

A canonical store slug, point the fleet at one merchant instead.

Response, 200

focusobjectrequired

The directive as recorded, what every developer now serves first.

7 child fields
areastring, nullablerequired

The pointed area slug (board_label_vocabulary), or null for a merchant focus.

merchantstring, nullablerequired

The pointed store slug, or null for an area focus.

labelstring, nullablerequired

The display name at point time ("Gym"), what banners and prompts say.

sectionsarray of stringrequired

The expanded predicate: sections half of the OR-union.

initiativesarray of stringrequired

The expanded predicate: initiatives half.

addonsarray of stringrequired

The expanded predicate: add-ons half.

pointed_atstringrequired

When the founder pointed it.

curl -X POST "https://www.membber.com/api/v1/board/fleet/focus" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "area": "<area>",
    "merchant": "<merchant>"
  }'
Response, 200
{
  "focus": {
    "area": "<area>",
    "merchant": "<merchant>",
    "label": "<label>",
    "sections": [
      "<section>"
    ],
    "initiatives": [
      "<initiative>"
    ],
    "addons": [
      "<addon>"
    ],
    "pointed_at": "<pointed_at>"
  }
}

Set how many developers the Mac runs by itself, or Stop it (0).

Writes the founder half of the board_fleet_target singleton (target_size / stopped_at / stop_reason) and records the change as a fleet:target activity row. The Mac launcher reads it on its next 15s tick and starts or stops accordingly, behind its own guards (free disk, a hard cap, a recorded halt, nothing-pickable, failure backoff, a ramp gap). 🔴 Stop (0) never kills a running developer, it stops NEW ones starting, so work in hand still lands. Setting a number above the launcher-observed headroom is refused with the real ceiling, never silently clamped.

Request body

target_sizeintegerrequired

How many developers the Mac should run by itself. 0 = Stop: nothing new starts.

stop_reasonstringoptional

Optional note recorded with a Stop (target_size 0), his words, for the activity row.

Response, 200

targetobjectrequired

The dial as recorded, the truth the card renders.

15 child fields
target_sizeintegerrequired

How many developers to run. 0 = nothing starts itself.

stopped_atstring, nullablerequired

When the founder last hit Stop; null = never, or restarted since.

stop_reasonstring, nullablerequired

His words for the stop, if he gave any.

headroom_maxinteger, nullablerequired

The honest live ceiling, launcher-written and pre-clamped to its hard cap. Null until the launcher has looked.

headroom_reasonstring, nullablerequired

What binds the ceiling: 'disk' (free space) | 'cap' (the most this Mac runs at once).

disk_free_gbnumber, nullablerequired

Free space on the Developer volume at the last decision, the reason behind the ceiling.

headroom_observed_atstring, nullablerequired

When the launcher last measured the ceiling.

last_refusalstring, nullablerequired

Why nothing started on the last tick that wanted to: halted|stopped|off|disk|at_target|no_work|load|memory|hard_cap|breaker|backoff|rate_cap|ramp. The app owns the human words.

last_refusal_atstring, nullablerequired

When that refusal was last recorded. Advances on every refusing tick even when the reason is unchanged, so a stale card is distinguishable from a dead daemon.

last_refusal_detailstring, nullablerequired

The refusal SENTENCE beside the code, carrying the numbers only the Mac can see ("8.4 GB free leaves no room for another"). Evidence, not copy: the app still owns the friendly words for the code.

consecutive_failuresintegerrequired

Auto-launch failures since a spawned session last said hello. At 3 the circuit breaker latches and only ONE probationary developer is tried per escalating gap (30m/1h/2h/4h), so a wall is waited out rather than hammered.

last_failure_atstring, nullablerequired

When the last auto-launch failure was recorded, read back by the launcher at startup so the breaker survives a daemon restart.

last_failure_detailstring, nullablerequired

What actually failed, in a sentence, so a stopped fleet explains itself without anyone opening a log.

last_auto_spawn_atstring, nullablerequired

When the launcher last started one by itself.

updated_atstringrequired
curl -X POST "https://www.membber.com/api/v1/board/fleet/target" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "target_size": 0
  }'
Response, 200
{
  "target": {
    "target_size": -9007199254740991,
    "stopped_at": "<stopped_at>",
    "stop_reason": "Added at the front desk",
    "headroom_max": -9007199254740991,
    "headroom_reason": "Added at the front desk",
    "disk_free_gb": 1,
    "headroom_observed_at": "<headroom_observed_at>",
    "last_refusal": "<last_refusal>",
    "last_refusal_at": "<last_refusal_at>",
    "last_refusal_detail": "<last_refusal_detail>",
    "consecutive_failures": -9007199254740991,
    "last_failure_at": "<last_failure_at>",
    "last_failure_detail": "<last_failure_detail>",
    "last_auto_spawn_at": "<last_auto_spawn_at>",
    "updated_at": "<updated_at>"
  }
}

Wake now: ask the Mac launcher to probe the CLI immediately.

Idempotent request marker, stamps wake_requested_at on the board_fleet_status singleton; the launcher consumes it, probes the claude CLI for real, and clears the halt only on an observed success. Safe to tap repeatedly: while genuinely still limited the probe is refused (costing nothing) and the halt stands. Wake itself never spawns anything: it only asks the launcher to look. Since 19 Aug 2026 the launcher DOES refill the fleet by itself once a halt clears, up to the board_fleet_target dial (default 0 = nothing auto-starts) and behind its own guards.

Response, 200

fleetobjectrequired
7 child fields
statusenumrequired

running = work can flow; halted_limit = the Mac CLI is refusing work on a usage limit.

runninghalted_limit
messagestring, nullablerequired

The verbatim captured limit message, or null while running.

reset_atstring, nullablerequired

Parsed from the real "resets …" clause; null when the message states no reset time.

observed_atstringrequired

When the launcher last looked at reality (bumped on every recheck while halted).

sourcestring, nullablerequired

What observed it: probe | spawn_probe | transcript | heartbeat | wake.

wake_requested_atstring, nullablerequired

The founder's un-consumed Wake-now tap, if one is pending.

updated_atstringrequired
curl -X POST "https://www.membber.com/api/v1/board/fleet/wake" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"
Response, 200
{
  "fleet": {
    "status": "running",
    "message": "Added at the front desk",
    "reset_at": "<reset_at>",
    "observed_at": "<observed_at>",
    "source": "<source>",
    "wake_requested_at": "<wake_requested_at>",
    "updated_at": "<updated_at>"
  }
}

The operator inbox: questions/decisions/updates surfaced by any thread, untriaged first then newest first.

Response, 200

inboxarray of objectrequired
18 child fields
idstringrequired
titlestringrequired
bodystringrequired
section_hintstring, nullablerequired
priority_hintstring, nullablerequired
reopen_ofstring, nullablerequired

The ticket id this inbox item re-opens, or null for a fresh item.

statusstringrequired

'untriaged', 'triaged' or 'answered'.

created_atstringrequired
triaged_atstring, nullablerequired
attachmentsarray of objectrequired

R19: the screenshots raised WITH it, before it is triaged into a ticket.

14 child fields
idstringrequired
ticket_idstring, nullablerequired
inbox_idstring, nullablerequired
kindstringrequired
captionstring, nullablerequired
widthinteger, nullablerequired
heightinteger, nullablerequired
created_atstringrequired
storage_pathstring, nullablerequired

The object in the private bucket. Stable; `url` is derived from it on every read. Null on a pure-link row.

external_urlstring, nullableoptional

The destination of a `kind='link'` row (a Figma page, a Loom). Null on stored rows.

urlstring, nullablerequired

For stored rows: a signed URL valid for one hour (null if signing failed; the row is still real). For a link row: `external_url` passed through untouched, one field to open, whatever the kind.

content_typestring, nullableoptional

MIME type of the stored object (e.g. video/quicktime, application/pdf). Null on legacy image rows.

filenamestring, nullableoptional

Original filename as picked on the device, for display. Null on screenshots.

byte_sizeinteger, nullableoptional

Verified size of the stored object in bytes. Null on legacy rows.

optionsarray of objectrequired

Decision options, or null for a plain (non-decision) inbox item.

3 child fields
keystringrequired

Stable machine key for this option (never shown to the founder).

labelstringrequired

The plain-English choice the founder reads and taps.

detailstring, nullableoptional

Optional one-line consequence under the label (DECISION-BRIEF step 4).

recommended_optionstring, nullablerequired

The asking thread's recommended option key, or null.

asking_session_idstring, nullablerequired

board_sessions.id of the thread that asked, so the answer can find its way back.

decision_kindenumrequired

How the founder answers this item: single (default) | multi | rank. Meaningless on a plain, option-less inbox item, but always present (defaults to single at the DB layer).

singlemultirank
answerobjectrequired

The founder's answer, or null while the decision is open.

5 child fields
kindenumoptional

The kind this was ANSWERED under, the truth of what was answered even if the item is later re-raised under a different decision_kind. Absent on rows answered before rich decisions shipped.

singlemultirank
option_keystringoptional

single: the chosen option key.

option_keysarray of stringoptional

multi: the picked set (order not meaningful). rank: the FULL order of every option key, best first.

textstringoptional

Free-text answer / rider, when given.

answered_bystringrequired

The verified operator email that answered, never client-supplied.

answered_atstring, nullablerequired
chatarray of objectrequired

The founder asking the raising thread a question WITHOUT resolving the decision (board_inbox_chat). Null = no chat yet. Never touches answer/answered_at/status.

3 child fields
textstringrequired
bystringrequired

Who sent it, the verified operator email, or the asking thread's session id.

atstringrequired

ISO timestamp.

ticket_idstring, nullablerequired

Optional link to the board ticket this decision blocks.

curl -G "https://www.membber.com/api/v1/board/inbox" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "inbox": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "title": "Morning class",
      "body": "<body>",
      "section_hint": "<section_hint>",
      "priority_hint": "<priority_hint>",
      "reopen_of": "<reopen_of>",
      "status": "<status>",
      "created_at": "<created_at>",
      "triaged_at": "<triaged_at>",
      "attachments": [
        {
          "id": "00000d1b-0000-4000-8000-d0c500000000",
          "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
          "inbox_id": "03d7f974-0000-4000-8000-d0c500000003",
          "kind": "<kind>",
          "caption": "<caption>",
          "width": -9007199254740991,
          "height": -9007199254740991,
          "created_at": "<created_at>",
          "storage_path": "<storage_path>",
          "external_url": "https://example.com/image.jpg",
          "url": "https://example.com/image.jpg",
          "content_type": "<content_type>",
          "filename": "<filename>",
          "byte_size": -9007199254740991
        }
      ],
      "options": [
        {
          "key": "<key>",
          "label": "<label>",
          "detail": "<detail>"
        }
      ],
      "recommended_option": "<recommended_option>",
      "asking_session_id": "50436e9a-0000-4000-8000-d0c500000050",
      "decision_kind": "single",
      "answer": {
        "kind": "single",
        "option_key": "<option_key>",
        "option_keys": [
          "<option_key>"
        ],
        "text": "<text>",
        "answered_by": "<answered_by>"
      },
      "answered_at": "<answered_at>",
      "chat": [
        {
          "text": "<text>",
          "by": "<by>",
          "at": "<at>"
        }
      ],
      "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb"
    }
  ]
}

Raise a decision, a plain update, or a bug with its screenshots, into the founder's inbox.

Two callers, one row. A THREAD raising a DECISION goes through the atomic board_inbox_raise RPC (write + board_activity in one statement); when options are present the founder's HQ devices also get an hq_decision push carrying the option keys and decision_kind. decision_kind (single | multi | rank, default single, since 20260816_110) picks how the founder answers: single is one tap, multi lets him tick several, rank has him drag the whole list into order, multi/rank REQUIRE options, rank needs at least 2. The FOUNDER raising a BUG (R19, "extreme importance") goes through board_raise_inbox instead, so the item and the screenshots that made him raise it land in ONE write, a second call that failed would leave a bug with no screenshot, or a screenshot with no bug. Idempotent either way: source_key for a decision, client_request_id for a bug.

Request body

titlestringrequired

One-line question/headline the founder sees first.

bodystringoptional

The DECISION BRIEF as prose: what we are doing, the scene, and what each option gives up. Optional for an R19 bug, a title (and its screenshots) alone is a complete report.

optionsarray of objectoptional

Decision options. Omit for a plain inbox update that needs no choice.

3 child fields
keystringrequired

Stable machine key for this option (never shown to the founder).

labelstringrequired

The plain-English choice the founder reads and taps.

detailstring, nullableoptional

Optional one-line consequence under the label (DECISION-BRIEF step 4).

recommended_optionstringoptional

The asking thread's recommendation, must be one of the option keys.

decision_kindenumoptional

How the founder should answer: single (default, one tap) | multi (tick several) | rank (drag every option into order). multi/rank REQUIRE options; rank requires at least 2.

singlemultirank
asking_session_idstringoptional

board_sessions.id of the asking thread, so the answer can find its way back.

ticket_idstringoptional

Optional link to the board ticket this decision blocks.

source_keystringoptional

Stable idempotency handle: a retried raise with the same source_key UPDATES the existing row instead of filing a duplicate (same mechanism the board-snapshot sync uses).

section_hintstringoptional

R19: where on the board this belongs, in his words.

priority_hintstringoptional

R19: how urgent he thinks it is.

reopen_ofstringoptional

The ticket this re-opens, when he is reporting that something shipped is broken again.

attachmentsarray of objectoptional

R19: the screenshots that made him raise it. They land in the SAME write as the item.

4 child fields
data_urlstringrequired

`data:image/png;base64,...`. PNG, JPEG, HEIC, WebP or GIF, up to 15 MB decoded.

captionstringoptional

What the screenshot shows, in his words.

kindenumoptional

Defaults to 'screenshot'.

screenshotimage
client_upload_idstringoptional

Idempotency key, same pattern as the messaging architecture's client_message_id. A retry with the same id returns the ORIGINAL attachment and writes no second history line, enforced by a unique index rather than promised in a comment.

kindenumoptional

What this row IS. "decision" is answerable and MUST carry >= 2 options, a recommendation and a brief; "note" is an update that asks him for nothing. Explicit rather than inferred from shape, so a plain update can never be promoted into his decision queue by accident. A diagnostic is not a valid value at all, machine signals go to the hang endpoint.

decisionnote
scenestringoptional

DECISION-BRIEF law step 2: a named person doing a real thing, so he does not have to construct the context himself. Required when kind is "decision".

why_yoursenumoptional

Why this is HIS call rather than the thread's. A question that cannot name one of these is not a founder decision, decide it and carry on. Required when kind is "decision".

tastemoneylegalpriorityirreversible
screenstringoptional

The HQ app screen that was frontmost when this was raised. Used SERVER-SIDE to file the row so he is never asked "where does this belong?", it is the strongest signal we hold.

client_request_idstringoptional

Idempotency key for the R19 bug path (stored as board_inbox.source_key, which carries a unique index). A retry after a dropped response returns the ORIGINAL item rather than filing the same bug twice. `source_key` is the same handle under the decision path's name.

Response, 200

inbox_itemobjectrequired
18 child fields
idstringrequired
titlestringrequired
bodystringrequired
section_hintstring, nullablerequired
priority_hintstring, nullablerequired
reopen_ofstring, nullablerequired

The ticket id this inbox item re-opens, or null for a fresh item.

statusstringrequired

'untriaged', 'triaged' or 'answered'.

created_atstringrequired
triaged_atstring, nullablerequired
attachmentsarray of objectrequired

R19: the screenshots raised WITH it, before it is triaged into a ticket.

14 child fields
idstringrequired
ticket_idstring, nullablerequired
inbox_idstring, nullablerequired
kindstringrequired
captionstring, nullablerequired
widthinteger, nullablerequired
heightinteger, nullablerequired
created_atstringrequired
storage_pathstring, nullablerequired

The object in the private bucket. Stable; `url` is derived from it on every read. Null on a pure-link row.

external_urlstring, nullableoptional

The destination of a `kind='link'` row (a Figma page, a Loom). Null on stored rows.

urlstring, nullablerequired

For stored rows: a signed URL valid for one hour (null if signing failed; the row is still real). For a link row: `external_url` passed through untouched, one field to open, whatever the kind.

content_typestring, nullableoptional

MIME type of the stored object (e.g. video/quicktime, application/pdf). Null on legacy image rows.

filenamestring, nullableoptional

Original filename as picked on the device, for display. Null on screenshots.

byte_sizeinteger, nullableoptional

Verified size of the stored object in bytes. Null on legacy rows.

optionsarray of objectrequired

Decision options, or null for a plain (non-decision) inbox item.

3 child fields
keystringrequired

Stable machine key for this option (never shown to the founder).

labelstringrequired

The plain-English choice the founder reads and taps.

detailstring, nullableoptional

Optional one-line consequence under the label (DECISION-BRIEF step 4).

recommended_optionstring, nullablerequired

The asking thread's recommended option key, or null.

asking_session_idstring, nullablerequired

board_sessions.id of the thread that asked, so the answer can find its way back.

decision_kindenumrequired

How the founder answers this item: single (default) | multi | rank. Meaningless on a plain, option-less inbox item, but always present (defaults to single at the DB layer).

singlemultirank
answerobjectrequired

The founder's answer, or null while the decision is open.

5 child fields
kindenumoptional

The kind this was ANSWERED under, the truth of what was answered even if the item is later re-raised under a different decision_kind. Absent on rows answered before rich decisions shipped.

singlemultirank
option_keystringoptional

single: the chosen option key.

option_keysarray of stringoptional

multi: the picked set (order not meaningful). rank: the FULL order of every option key, best first.

textstringoptional

Free-text answer / rider, when given.

answered_bystringrequired

The verified operator email that answered, never client-supplied.

answered_atstring, nullablerequired
chatarray of objectrequired

The founder asking the raising thread a question WITHOUT resolving the decision (board_inbox_chat). Null = no chat yet. Never touches answer/answered_at/status.

3 child fields
textstringrequired
bystringrequired

Who sent it, the verified operator email, or the asking thread's session id.

atstringrequired

ISO timestamp.

ticket_idstring, nullablerequired

Optional link to the board ticket this decision blocks.

curl -X POST "https://www.membber.com/api/v1/board/inbox" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Morning class"
  }'
Response, 200
{
  "inbox_item": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "title": "Morning class",
    "body": "<body>",
    "section_hint": "<section_hint>",
    "priority_hint": "<priority_hint>",
    "reopen_of": "<reopen_of>",
    "status": "<status>",
    "created_at": "<created_at>",
    "triaged_at": "<triaged_at>",
    "attachments": [
      {
        "id": "00000d1b-0000-4000-8000-d0c500000000",
        "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
        "inbox_id": "03d7f974-0000-4000-8000-d0c500000003",
        "kind": "<kind>",
        "caption": "<caption>",
        "width": -9007199254740991,
        "height": -9007199254740991,
        "created_at": "<created_at>",
        "storage_path": "<storage_path>",
        "external_url": "https://example.com/image.jpg",
        "url": "https://example.com/image.jpg",
        "content_type": "<content_type>",
        "filename": "<filename>",
        "byte_size": -9007199254740991
      }
    ],
    "options": [
      {
        "key": "<key>",
        "label": "<label>",
        "detail": "<detail>"
      }
    ],
    "recommended_option": "<recommended_option>",
    "asking_session_id": "50436e9a-0000-4000-8000-d0c500000050",
    "decision_kind": "single",
    "answer": {
      "kind": "single",
      "option_key": "<option_key>",
      "option_keys": [
        "<option_key>"
      ],
      "text": "<text>",
      "answered_by": "<answered_by>"
    },
    "answered_at": "<answered_at>",
    "chat": [
      {
        "text": "<text>",
        "by": "<by>",
        "at": "<at>"
      }
    ],
    "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb"
  }
}

Answer a decision inbox item, the founder's call, recorded once.

Atomic board_inbox_answer RPC: writes answer + answered_at + status=answered and logs activity under inbox:<id> AND session:<asking_session_id> so the asking thread can find it. Idempotent, the SAME answer again returns ok; a DIFFERENT answer to an answered decision is a 409 (the first call stands). Kind-aware since rich decisions (20260816_110): single takes option_key, multi/rank take option_keys (multi = the picked set, rank = a full permutation of every option, best first).

Parameters

idstringpathrequired

Request body

option_keystringoptional

single: the chosen option key (one of the decision's options).

option_keysarray of stringoptional

multi: the picked set (≥1, every key must be one of the options, no duplicates). rank: the FULL order of every option key, best first, a partial list, an unknown key, or a repeat is refused by the RPC (NOT_A_PERMUTATION / UNKNOWN_OPTION / DUPLICATE_OPTION).

textstringoptional

Free-text answer, or a rider alongside the chosen option(s).

Response, 200

inbox_itemobjectrequired
18 child fields
idstringrequired
titlestringrequired
bodystringrequired
section_hintstring, nullablerequired
priority_hintstring, nullablerequired
reopen_ofstring, nullablerequired

The ticket id this inbox item re-opens, or null for a fresh item.

statusstringrequired

'untriaged', 'triaged' or 'answered'.

created_atstringrequired
triaged_atstring, nullablerequired
attachmentsarray of objectrequired

R19: the screenshots raised WITH it, before it is triaged into a ticket.

14 child fields
idstringrequired
ticket_idstring, nullablerequired
inbox_idstring, nullablerequired
kindstringrequired
captionstring, nullablerequired
widthinteger, nullablerequired
heightinteger, nullablerequired
created_atstringrequired
storage_pathstring, nullablerequired

The object in the private bucket. Stable; `url` is derived from it on every read. Null on a pure-link row.

external_urlstring, nullableoptional

The destination of a `kind='link'` row (a Figma page, a Loom). Null on stored rows.

urlstring, nullablerequired

For stored rows: a signed URL valid for one hour (null if signing failed; the row is still real). For a link row: `external_url` passed through untouched, one field to open, whatever the kind.

content_typestring, nullableoptional

MIME type of the stored object (e.g. video/quicktime, application/pdf). Null on legacy image rows.

filenamestring, nullableoptional

Original filename as picked on the device, for display. Null on screenshots.

byte_sizeinteger, nullableoptional

Verified size of the stored object in bytes. Null on legacy rows.

optionsarray of objectrequired

Decision options, or null for a plain (non-decision) inbox item.

3 child fields
keystringrequired

Stable machine key for this option (never shown to the founder).

labelstringrequired

The plain-English choice the founder reads and taps.

detailstring, nullableoptional

Optional one-line consequence under the label (DECISION-BRIEF step 4).

recommended_optionstring, nullablerequired

The asking thread's recommended option key, or null.

asking_session_idstring, nullablerequired

board_sessions.id of the thread that asked, so the answer can find its way back.

decision_kindenumrequired

How the founder answers this item: single (default) | multi | rank. Meaningless on a plain, option-less inbox item, but always present (defaults to single at the DB layer).

singlemultirank
answerobjectrequired

The founder's answer, or null while the decision is open.

5 child fields
kindenumoptional

The kind this was ANSWERED under, the truth of what was answered even if the item is later re-raised under a different decision_kind. Absent on rows answered before rich decisions shipped.

singlemultirank
option_keystringoptional

single: the chosen option key.

option_keysarray of stringoptional

multi: the picked set (order not meaningful). rank: the FULL order of every option key, best first.

textstringoptional

Free-text answer / rider, when given.

answered_bystringrequired

The verified operator email that answered, never client-supplied.

answered_atstring, nullablerequired
chatarray of objectrequired

The founder asking the raising thread a question WITHOUT resolving the decision (board_inbox_chat). Null = no chat yet. Never touches answer/answered_at/status.

3 child fields
textstringrequired
bystringrequired

Who sent it, the verified operator email, or the asking thread's session id.

atstringrequired

ISO timestamp.

ticket_idstring, nullablerequired

Optional link to the board ticket this decision blocks.

curl -X POST "https://www.membber.com/api/v1/board/inbox/00000d1b-0000-4000-8000-d0c500000000/answer" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "option_key": "<option_key>",
    "option_keys": [
      "<option_key>"
    ],
    "text": "<text>"
  }'
Response, 200
{
  "inbox_item": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "title": "Morning class",
    "body": "<body>",
    "section_hint": "<section_hint>",
    "priority_hint": "<priority_hint>",
    "reopen_of": "<reopen_of>",
    "status": "<status>",
    "created_at": "<created_at>",
    "triaged_at": "<triaged_at>",
    "attachments": [
      {
        "id": "00000d1b-0000-4000-8000-d0c500000000",
        "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
        "inbox_id": "03d7f974-0000-4000-8000-d0c500000003",
        "kind": "<kind>",
        "caption": "<caption>",
        "width": -9007199254740991,
        "height": -9007199254740991,
        "created_at": "<created_at>",
        "storage_path": "<storage_path>",
        "external_url": "https://example.com/image.jpg",
        "url": "https://example.com/image.jpg",
        "content_type": "<content_type>",
        "filename": "<filename>",
        "byte_size": -9007199254740991
      }
    ],
    "options": [
      {
        "key": "<key>",
        "label": "<label>",
        "detail": "<detail>"
      }
    ],
    "recommended_option": "<recommended_option>",
    "asking_session_id": "50436e9a-0000-4000-8000-d0c500000050",
    "decision_kind": "single",
    "answer": {
      "kind": "single",
      "option_key": "<option_key>",
      "option_keys": [
        "<option_key>"
      ],
      "text": "<text>",
      "answered_by": "<answered_by>"
    },
    "answered_at": "<answered_at>",
    "chat": [
      {
        "text": "<text>",
        "by": "<by>",
        "at": "<at>"
      }
    ],
    "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb"
  }
}

Every screenshot on an inbox item, with fresh signed URLs.

auth operator:boardop listBoardInboxAttachmentsrate limitederrors

Parameters

idstringpathrequired

Response, 200

attachmentsarray of objectrequired
14 child fields
idstringrequired
ticket_idstring, nullablerequired
inbox_idstring, nullablerequired
kindstringrequired
captionstring, nullablerequired
widthinteger, nullablerequired
heightinteger, nullablerequired
created_atstringrequired
storage_pathstring, nullablerequired

The object in the private bucket. Stable; `url` is derived from it on every read. Null on a pure-link row.

external_urlstring, nullableoptional

The destination of a `kind='link'` row (a Figma page, a Loom). Null on stored rows.

urlstring, nullablerequired

For stored rows: a signed URL valid for one hour (null if signing failed; the row is still real). For a link row: `external_url` passed through untouched, one field to open, whatever the kind.

content_typestring, nullableoptional

MIME type of the stored object (e.g. video/quicktime, application/pdf). Null on legacy image rows.

filenamestring, nullableoptional

Original filename as picked on the device, for display. Null on screenshots.

byte_sizeinteger, nullableoptional

Verified size of the stored object in bytes. Null on legacy rows.

curl -G "https://www.membber.com/api/v1/board/inbox/00000d1b-0000-4000-8000-d0c500000000/attachments" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "attachments": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
      "inbox_id": "03d7f974-0000-4000-8000-d0c500000003",
      "kind": "<kind>",
      "caption": "<caption>",
      "width": -9007199254740991,
      "height": -9007199254740991,
      "created_at": "<created_at>",
      "storage_path": "<storage_path>",
      "external_url": "https://example.com/image.jpg",
      "url": "https://example.com/image.jpg",
      "content_type": "<content_type>",
      "filename": "<filename>",
      "byte_size": -9007199254740991
    }
  ]
}

Attach a screenshot (inline) or a pre-uploaded video/file to an inbox item.

R19/R20. The one-shot path is raiseBoardDecision, which carries the screenshots with the bug. This is for adding one afterwards, when he spots a second symptom of something he has already filed.

Parameters

idstringpathrequired

Response, 200

attachmentobjectrequired
14 child fields
idstringrequired
ticket_idstring, nullablerequired
inbox_idstring, nullablerequired
kindstringrequired
captionstring, nullablerequired
widthinteger, nullablerequired
heightinteger, nullablerequired
created_atstringrequired
storage_pathstring, nullablerequired

The object in the private bucket. Stable; `url` is derived from it on every read. Null on a pure-link row.

external_urlstring, nullableoptional

The destination of a `kind='link'` row (a Figma page, a Loom). Null on stored rows.

urlstring, nullablerequired

For stored rows: a signed URL valid for one hour (null if signing failed; the row is still real). For a link row: `external_url` passed through untouched, one field to open, whatever the kind.

content_typestring, nullableoptional

MIME type of the stored object (e.g. video/quicktime, application/pdf). Null on legacy image rows.

filenamestring, nullableoptional

Original filename as picked on the device, for display. Null on screenshots.

byte_sizeinteger, nullableoptional

Verified size of the stored object in bytes. Null on legacy rows.

curl -X POST "https://www.membber.com/api/v1/board/inbox/00000d1b-0000-4000-8000-d0c500000000/attachments" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '"<value>"'
Response, 200
{
  "attachment": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
    "inbox_id": "03d7f974-0000-4000-8000-d0c500000003",
    "kind": "<kind>",
    "caption": "<caption>",
    "width": -9007199254740991,
    "height": -9007199254740991,
    "created_at": "<created_at>",
    "storage_path": "<storage_path>",
    "external_url": "https://example.com/image.jpg",
    "url": "https://example.com/image.jpg",
    "content_type": "<content_type>",
    "filename": "<filename>",
    "byte_size": -9007199254740991
  }
}

Mint a direct-to-storage upload URL for a video or file going onto an inbox item.

Step 1 of the large-media path for a bug already raised: PUT the bytes to `signed_url`, then POST …/attachments with `storage_path` to register them. Same shape as the ticket variant.

Parameters

idstringpathrequired

Request body

kindenumrequired

What the bytes are. Drives validation + how the app renders the tile.

imagevideofile
content_typestringrequired

The file's MIME type (e.g. video/quicktime, application/pdf).

byte_sizeintegerrequired

Size of the upload in bytes. Images cap at 15 MB, videos/files at 50 MB (the storage ceiling).

filenamestringoptional

Original filename, for display on the ticket.

Response, 200

pathstringrequired

The object path the upload MUST land at, registration refuses any other.

tokenstringrequired

Signed-upload token (storage `upload/sign`).

signed_urlstringrequired

Absolute URL to PUT the bytes to.

curl -X POST "https://www.membber.com/api/v1/board/inbox/00000d1b-0000-4000-8000-d0c500000000/attachments/upload-url" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "image",
    "content_type": "<content_type>",
    "byte_size": 1
  }'
Response, 200
{
  "path": "<path>",
  "token": "<token>",
  "signed_url": "https://example.com/image.jpg"
}

Ask the raising thread a question about this decision, WITHOUT resolving it.

Atomic board_inbox_chat RPC: appends one {text, by, at} entry to `chat` and logs activity under inbox:<id> AND session:<asking_session_id>, so the asking thread's `board-thread.mjs answers` surfaces it as a kind:'chat' line. Deliberately never touches answer/answered_at/status, the decision stays open, exactly the founder's "ask a question back" case from the CLI AskUserQuestion exemplar.

Parameters

idstringpathrequired

Request body

textstringrequired

The question / message for the raising thread.

Response, 200

inbox_itemobjectrequired
18 child fields
idstringrequired
titlestringrequired
bodystringrequired
section_hintstring, nullablerequired
priority_hintstring, nullablerequired
reopen_ofstring, nullablerequired

The ticket id this inbox item re-opens, or null for a fresh item.

statusstringrequired

'untriaged', 'triaged' or 'answered'.

created_atstringrequired
triaged_atstring, nullablerequired
attachmentsarray of objectrequired

R19: the screenshots raised WITH it, before it is triaged into a ticket.

14 child fields
idstringrequired
ticket_idstring, nullablerequired
inbox_idstring, nullablerequired
kindstringrequired
captionstring, nullablerequired
widthinteger, nullablerequired
heightinteger, nullablerequired
created_atstringrequired
storage_pathstring, nullablerequired

The object in the private bucket. Stable; `url` is derived from it on every read. Null on a pure-link row.

external_urlstring, nullableoptional

The destination of a `kind='link'` row (a Figma page, a Loom). Null on stored rows.

urlstring, nullablerequired

For stored rows: a signed URL valid for one hour (null if signing failed; the row is still real). For a link row: `external_url` passed through untouched, one field to open, whatever the kind.

content_typestring, nullableoptional

MIME type of the stored object (e.g. video/quicktime, application/pdf). Null on legacy image rows.

filenamestring, nullableoptional

Original filename as picked on the device, for display. Null on screenshots.

byte_sizeinteger, nullableoptional

Verified size of the stored object in bytes. Null on legacy rows.

optionsarray of objectrequired

Decision options, or null for a plain (non-decision) inbox item.

3 child fields
keystringrequired

Stable machine key for this option (never shown to the founder).

labelstringrequired

The plain-English choice the founder reads and taps.

detailstring, nullableoptional

Optional one-line consequence under the label (DECISION-BRIEF step 4).

recommended_optionstring, nullablerequired

The asking thread's recommended option key, or null.

asking_session_idstring, nullablerequired

board_sessions.id of the thread that asked, so the answer can find its way back.

decision_kindenumrequired

How the founder answers this item: single (default) | multi | rank. Meaningless on a plain, option-less inbox item, but always present (defaults to single at the DB layer).

singlemultirank
answerobjectrequired

The founder's answer, or null while the decision is open.

5 child fields
kindenumoptional

The kind this was ANSWERED under, the truth of what was answered even if the item is later re-raised under a different decision_kind. Absent on rows answered before rich decisions shipped.

singlemultirank
option_keystringoptional

single: the chosen option key.

option_keysarray of stringoptional

multi: the picked set (order not meaningful). rank: the FULL order of every option key, best first.

textstringoptional

Free-text answer / rider, when given.

answered_bystringrequired

The verified operator email that answered, never client-supplied.

answered_atstring, nullablerequired
chatarray of objectrequired

The founder asking the raising thread a question WITHOUT resolving the decision (board_inbox_chat). Null = no chat yet. Never touches answer/answered_at/status.

3 child fields
textstringrequired
bystringrequired

Who sent it, the verified operator email, or the asking thread's session id.

atstringrequired

ISO timestamp.

ticket_idstring, nullablerequired

Optional link to the board ticket this decision blocks.

curl -X POST "https://www.membber.com/api/v1/board/inbox/00000d1b-0000-4000-8000-d0c500000000/chat" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "<text>"
  }'
Response, 200
{
  "inbox_item": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "title": "Morning class",
    "body": "<body>",
    "section_hint": "<section_hint>",
    "priority_hint": "<priority_hint>",
    "reopen_of": "<reopen_of>",
    "status": "<status>",
    "created_at": "<created_at>",
    "triaged_at": "<triaged_at>",
    "attachments": [
      {
        "id": "00000d1b-0000-4000-8000-d0c500000000",
        "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
        "inbox_id": "03d7f974-0000-4000-8000-d0c500000003",
        "kind": "<kind>",
        "caption": "<caption>",
        "width": -9007199254740991,
        "height": -9007199254740991,
        "created_at": "<created_at>",
        "storage_path": "<storage_path>",
        "external_url": "https://example.com/image.jpg",
        "url": "https://example.com/image.jpg",
        "content_type": "<content_type>",
        "filename": "<filename>",
        "byte_size": -9007199254740991
      }
    ],
    "options": [
      {
        "key": "<key>",
        "label": "<label>",
        "detail": "<detail>"
      }
    ],
    "recommended_option": "<recommended_option>",
    "asking_session_id": "50436e9a-0000-4000-8000-d0c500000050",
    "decision_kind": "single",
    "answer": {
      "kind": "single",
      "option_key": "<option_key>",
      "option_keys": [
        "<option_key>"
      ],
      "text": "<text>",
      "answered_by": "<answered_by>"
    },
    "answered_at": "<answered_at>",
    "chat": [
      {
        "text": "<text>",
        "by": "<by>",
        "at": "<at>"
      }
    ],
    "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb"
  }
}

Triage an inbox item: assign section + priority and mark it triaged.

Parameters

idstringpathrequired

Request body

sectionstringrequired

The section to file this inbox item under.

prioritystringrequired

The priority to assign to this inbox item.

Response, 200

inbox_itemobjectrequired
18 child fields
idstringrequired
titlestringrequired
bodystringrequired
section_hintstring, nullablerequired
priority_hintstring, nullablerequired
reopen_ofstring, nullablerequired

The ticket id this inbox item re-opens, or null for a fresh item.

statusstringrequired

'untriaged', 'triaged' or 'answered'.

created_atstringrequired
triaged_atstring, nullablerequired
attachmentsarray of objectrequired

R19: the screenshots raised WITH it, before it is triaged into a ticket.

14 child fields
idstringrequired
ticket_idstring, nullablerequired
inbox_idstring, nullablerequired
kindstringrequired
captionstring, nullablerequired
widthinteger, nullablerequired
heightinteger, nullablerequired
created_atstringrequired
storage_pathstring, nullablerequired

The object in the private bucket. Stable; `url` is derived from it on every read. Null on a pure-link row.

external_urlstring, nullableoptional

The destination of a `kind='link'` row (a Figma page, a Loom). Null on stored rows.

urlstring, nullablerequired

For stored rows: a signed URL valid for one hour (null if signing failed; the row is still real). For a link row: `external_url` passed through untouched, one field to open, whatever the kind.

content_typestring, nullableoptional

MIME type of the stored object (e.g. video/quicktime, application/pdf). Null on legacy image rows.

filenamestring, nullableoptional

Original filename as picked on the device, for display. Null on screenshots.

byte_sizeinteger, nullableoptional

Verified size of the stored object in bytes. Null on legacy rows.

optionsarray of objectrequired

Decision options, or null for a plain (non-decision) inbox item.

3 child fields
keystringrequired

Stable machine key for this option (never shown to the founder).

labelstringrequired

The plain-English choice the founder reads and taps.

detailstring, nullableoptional

Optional one-line consequence under the label (DECISION-BRIEF step 4).

recommended_optionstring, nullablerequired

The asking thread's recommended option key, or null.

asking_session_idstring, nullablerequired

board_sessions.id of the thread that asked, so the answer can find its way back.

decision_kindenumrequired

How the founder answers this item: single (default) | multi | rank. Meaningless on a plain, option-less inbox item, but always present (defaults to single at the DB layer).

singlemultirank
answerobjectrequired

The founder's answer, or null while the decision is open.

5 child fields
kindenumoptional

The kind this was ANSWERED under, the truth of what was answered even if the item is later re-raised under a different decision_kind. Absent on rows answered before rich decisions shipped.

singlemultirank
option_keystringoptional

single: the chosen option key.

option_keysarray of stringoptional

multi: the picked set (order not meaningful). rank: the FULL order of every option key, best first.

textstringoptional

Free-text answer / rider, when given.

answered_bystringrequired

The verified operator email that answered, never client-supplied.

answered_atstring, nullablerequired
chatarray of objectrequired

The founder asking the raising thread a question WITHOUT resolving the decision (board_inbox_chat). Null = no chat yet. Never touches answer/answered_at/status.

3 child fields
textstringrequired
bystringrequired

Who sent it, the verified operator email, or the asking thread's session id.

atstringrequired

ISO timestamp.

ticket_idstring, nullablerequired

Optional link to the board ticket this decision blocks.

curl -X POST "https://www.membber.com/api/v1/board/inbox/00000d1b-0000-4000-8000-d0c500000000/triage" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "section": "<section>",
    "priority": "<priority>"
  }'
Response, 200
{
  "inbox_item": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "title": "Morning class",
    "body": "<body>",
    "section_hint": "<section_hint>",
    "priority_hint": "<priority_hint>",
    "reopen_of": "<reopen_of>",
    "status": "<status>",
    "created_at": "<created_at>",
    "triaged_at": "<triaged_at>",
    "attachments": [
      {
        "id": "00000d1b-0000-4000-8000-d0c500000000",
        "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
        "inbox_id": "03d7f974-0000-4000-8000-d0c500000003",
        "kind": "<kind>",
        "caption": "<caption>",
        "width": -9007199254740991,
        "height": -9007199254740991,
        "created_at": "<created_at>",
        "storage_path": "<storage_path>",
        "external_url": "https://example.com/image.jpg",
        "url": "https://example.com/image.jpg",
        "content_type": "<content_type>",
        "filename": "<filename>",
        "byte_size": -9007199254740991
      }
    ],
    "options": [
      {
        "key": "<key>",
        "label": "<label>",
        "detail": "<detail>"
      }
    ],
    "recommended_option": "<recommended_option>",
    "asking_session_id": "50436e9a-0000-4000-8000-d0c500000050",
    "decision_kind": "single",
    "answer": {
      "kind": "single",
      "option_key": "<option_key>",
      "option_keys": [
        "<option_key>"
      ],
      "text": "<text>",
      "answered_by": "<answered_by>"
    },
    "answered_at": "<answered_at>",
    "chat": [
      {
        "text": "<text>",
        "by": "<by>",
        "at": "<at>"
      }
    ],
    "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb"
  }
}

The founder's section priority order (board_section_order).

Worker threads may read this when picking what to build next: sections earlier in the list matter more to the founder right now. The HQ app renders its Sections grouping and Progress surface in this order.

Response, 200

orderarray of stringrequired

The founder's priority order of the board sections.

curl -G "https://www.membber.com/api/v1/board/sections/order" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "order": [
    "<order>"
  ]
}

Set the founder's section priority order.

Request body

orderarray of stringrequired

The full section list in the new priority order (deduplicated server-side).

Response, 200

orderarray of stringrequired

The founder's priority order of the board sections.

curl -X PUT "https://www.membber.com/api/v1/board/sections/order" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "order": [
      "<order>"
    ]
  }'
Response, 200
{
  "order": [
    "<order>"
  ]
}

Sign in to the Membber HQ board with email + password.

Verifies the email/password against Supabase auth, then checks the founder allowlist. On success mints a short-lived operator token. A correct password for a non-allow-listed address is a refusal (403), not a login.

auth none, publicop boardLoginidempotent retryrate limitederrors

Request body

emailstringrequired

The operator email (verified against Supabase auth).

passwordstringrequired

The account password, verified against Supabase, never stored here.

Response, 200

tokenstringrequired

The operator bearer token to send as Authorization: Bearer on every board call.

mestringrequired

The verified, allow-listed operator email.

expiresAtstringrequired

ISO timestamp the token expires.

curl -X POST "https://www.membber.com/api/v1/board/session" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alex@example.com",
    "password": "<password>"
  }'
Response, 200
{
  "token": "<token>",
  "me": "<me>",
  "expiresAt": "<expiresAt>"
}

Sign in to the Membber HQ board with Sign in with Apple.

Verifies the Apple identity token fully (RS256 signature against Apple's JWKS, issuer, audience com.membber.hq, expiry), then checks the founder allowlist on the verified email. A missing / relay / non-allow-listed email is a 403 that echoes the email + Apple subject it saw, so the founder can add it. On success mints the same operator token.

auth none, publicop boardLoginAppleidempotent retryrate limitederrors

Request body

identityTokenstringrequired

The Sign in with Apple identity token (JWT) returned to the app.

Response, 200

tokenstringrequired

The operator bearer token to send as Authorization: Bearer on every board call.

mestringrequired

The verified, allow-listed operator email.

expiresAtstringrequired

ISO timestamp the token expires.

curl -X POST "https://www.membber.com/api/v1/board/session/apple" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "identityToken": "<identityToken>"
  }'
Response, 200
{
  "token": "<token>",
  "me": "<me>",
  "expiresAt": "<expiresAt>"
}

Sign in to the Membber HQ board with Google.

Verifies the Google ID token fully (RS256 signature against Google's JWKS via jwks-rsa, issuer accounts.google.com, audience the Membber Google client id, expiry) using the SAME production verifier the App Clip customer Google sign-in uses, src/lib/auth/googleAuth.ts verifyGoogleIdToken. Then checks the founder allowlist on the verified email. A missing / unverified / non-allow-listed email is a 403 that echoes the email + Google subject it saw, so the founder can add it. On success mints the same operator token.

auth none, publicop boardLoginGoogleidempotent retryrate limitederrors

Request body

credentialstringrequired

The Google ID token (JWT) returned to the app.

Response, 200

tokenstringrequired

The operator bearer token to send as Authorization: Bearer on every board call.

mestringrequired

The verified, allow-listed operator email.

expiresAtstringrequired

ISO timestamp the token expires.

curl -X POST "https://www.membber.com/api/v1/board/session/google" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "credential": "<credential>"
  }'
Response, 200
{
  "token": "<token>",
  "me": "<me>",
  "expiresAt": "<expiresAt>"
}

Every thread, live ones first, each with what it is holding per column.

R24-R26. A thread is a developer, so this is the team list: who is live, on what branch, and how many tickets each has in To-do, Doing, Review, Testing and Done.

Parameters

include_finishedbooleanqueryoptional

Default TRUE, which returns finished threads too. The "Live threads" filter does not need them, but a session picker does: a ticket can still be assigned to a thread that is currently asleep. Pass false for live-only, which is the same thing live_only=true asks for.

live_onlybooleanqueryoptional

R24/R25: only threads that are live and not finished. Equivalent to include_finished=false.

stale_after_minutesintegerqueryoptional

How long without a heartbeat before a thread stops counting as live. Default 20.

Response, 200

sessionsarray of objectrequired
19 child fields
session_idstringrequired
labelstring, nullablerequired

The human-readable name of the thread, or null if it never sent one.

kindstringrequired

'ai' or 'human'.

branchstring, nullablerequired
worktreestring, nullablerequired
started_atstringrequired
last_seen_atstringrequired
finished_atstring, nullablerequired
is_livebooleanrequired

R24: not finished AND seen inside the staleness window. This is what "a live session" means.

ticket_idsarray of stringrequired
in_progressintegerrequired

R25: how many tickets this thread holds in Doing.

in_reviewintegerrequired
testingintegerrequired
todointegerrequired
doneintegerrequired
goalstring, nullablerequired

The mission this thread carries, in the words it was given ("fix click and collect bugs"). From the kit's `hello --goal`, or the spawn request's instructions for a phone-spawned thread. Null when never stated, a client renders nothing, never an invented mission.

filed_countintegerrequired

Tickets this developer FILED, counted across every real linkage (content_source `thread:<label>` + activity `filed` rows), not just live session_id ownership. The zero-tickets fix (17 Aug 2026): 28 of 38 developers read zero under the session_id-only join while 27 of them had real history on other linkages.

finished_countintegerrequired

Linked tickets now sitting in Done, across every linkage.

history_countintegerrequired

Distinct tickets with ANY real linkage to this developer (held / filed / touched / branch). Views excluded.

curl -G "https://www.membber.com/api/v1/board/sessions" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "sessions": [
    {
      "session_id": "630ddf64-0000-4000-8000-d0c500000063",
      "label": "<label>",
      "kind": "<kind>",
      "branch": "<branch>",
      "worktree": "<worktree>",
      "started_at": "<started_at>",
      "last_seen_at": "<last_seen_at>",
      "finished_at": "<finished_at>",
      "is_live": true,
      "ticket_ids": [
        "fbc29b2e-0000-4000-8000-d0c5000000fb"
      ],
      "in_progress": -9007199254740991,
      "in_review": -9007199254740991,
      "testing": -9007199254740991,
      "todo": -9007199254740991,
      "done": -9007199254740991,
      "goal": "<goal>",
      "filed_count": 1,
      "finished_count": 1,
      "history_count": 1
    }
  ]
}

A thread says it has stopped.

R25: the live filter is "live and NOT finished". Idempotent, and the FIRST finish time is kept, so a repeated call does not quietly move the clock. A later heartbeat revives the thread.

Parameters

idstringpathrequired

Response, 200

sessionobjectrequired
19 child fields
session_idstringrequired
labelstring, nullablerequired

The human-readable name of the thread, or null if it never sent one.

kindstringrequired

'ai' or 'human'.

branchstring, nullablerequired
worktreestring, nullablerequired
started_atstringrequired
last_seen_atstringrequired
finished_atstring, nullablerequired
is_livebooleanrequired

R24: not finished AND seen inside the staleness window. This is what "a live session" means.

ticket_idsarray of stringrequired
in_progressintegerrequired

R25: how many tickets this thread holds in Doing.

in_reviewintegerrequired
testingintegerrequired
todointegerrequired
doneintegerrequired
goalstring, nullablerequired

The mission this thread carries, in the words it was given ("fix click and collect bugs"). From the kit's `hello --goal`, or the spawn request's instructions for a phone-spawned thread. Null when never stated, a client renders nothing, never an invented mission.

filed_countintegerrequired

Tickets this developer FILED, counted across every real linkage (content_source `thread:<label>` + activity `filed` rows), not just live session_id ownership. The zero-tickets fix (17 Aug 2026): 28 of 38 developers read zero under the session_id-only join while 27 of them had real history on other linkages.

finished_countintegerrequired

Linked tickets now sitting in Done, across every linkage.

history_countintegerrequired

Distinct tickets with ANY real linkage to this developer (held / filed / touched / branch). Views excluded.

curl -X POST "https://www.membber.com/api/v1/board/sessions/00000d1b-0000-4000-8000-d0c500000000/finish" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"
Response, 200
{
  "session": {
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "label": "<label>",
    "kind": "<kind>",
    "branch": "<branch>",
    "worktree": "<worktree>",
    "started_at": "<started_at>",
    "last_seen_at": "<last_seen_at>",
    "finished_at": "<finished_at>",
    "is_live": true,
    "ticket_ids": [
      "fbc29b2e-0000-4000-8000-d0c5000000fb"
    ],
    "in_progress": -9007199254740991,
    "in_review": -9007199254740991,
    "testing": -9007199254740991,
    "todo": -9007199254740991,
    "done": -9007199254740991,
    "goal": "<goal>",
    "filed_count": 1,
    "finished_count": 1,
    "history_count": 1
  }
}

A developer's full record: every ticket they filed, held, moved, finished or touched.

The zero-tickets fix (17 Aug 2026). The roster join (board_tickets.session_id) only knows live ownership, 23 of 608 tickets, while the real record lives in content_source, board_activity, board_comments, board_ticket_views and branch matches. This unions ALL of them (board_session_history RPC) so the Developers detail can show what a thread actually did. Aggregates are computed from the same rows: nothing here is ever invented (no-fake-data law).

auth operator:boardop getBoardSessionHistoryrate limitederrors

Parameters

idstringpathrequired

Response, 200

sessionobjectrequired
19 child fields
session_idstringrequired
labelstring, nullablerequired

The human-readable name of the thread, or null if it never sent one.

kindstringrequired

'ai' or 'human'.

branchstring, nullablerequired
worktreestring, nullablerequired
started_atstringrequired
last_seen_atstringrequired
finished_atstring, nullablerequired
is_livebooleanrequired

R24: not finished AND seen inside the staleness window. This is what "a live session" means.

ticket_idsarray of stringrequired
in_progressintegerrequired

R25: how many tickets this thread holds in Doing.

in_reviewintegerrequired
testingintegerrequired
todointegerrequired
doneintegerrequired
goalstring, nullablerequired

The mission this thread carries, in the words it was given ("fix click and collect bugs"). From the kit's `hello --goal`, or the spawn request's instructions for a phone-spawned thread. Null when never stated, a client renders nothing, never an invented mission.

filed_countintegerrequired

Tickets this developer FILED, counted across every real linkage (content_source `thread:<label>` + activity `filed` rows), not just live session_id ownership. The zero-tickets fix (17 Aug 2026): 28 of 38 developers read zero under the session_id-only join while 27 of them had real history on other linkages.

finished_countintegerrequired

Linked tickets now sitting in Done, across every linkage.

history_countintegerrequired

Distinct tickets with ANY real linkage to this developer (held / filed / touched / branch). Views excluded.

historyarray of objectrequired

Every ticket with any real linkage, newest event first. Empty = genuinely no trace, never hidden work.

13 child fields
ticket_idstringrequired
titlestring, nullablerequired
colstring, nullablerequired

Resolved column (founder overlay wins), so "Finished" is the truth.

typestringrequired
priostringrequired
sectionstring, nullablerequired
initiativestring, nullablerequired

The initiative label when classified (platform, click-collect, gym, …).

reopen_countintegerrequired
linkagearray of stringrequired

Every way this ticket ties to the developer: held (session_id / assignee label) · filed (content_source) · activity (any activity row) · commented · viewed · branch. Real rows only.

verbsarray of stringrequired

Distinct activity verbs this developer performed on it: filed, moved, claimed, assigned, …

first_event_atstring, nullablerequired
last_event_atstring, nullablerequired
view_countintegerrequired
filed_countintegerrequired
finished_countintegerrequired
sectionsarray of stringrequired

Distinct sections over the linked tickets, most-touched first.

initiativesarray of stringrequired

Distinct initiatives over the linked tickets, most-touched first.

first_activity_atstring, nullablerequired

The developer's earliest recorded event, with last_seen_at it is the honest activity span.

curl -G "https://www.membber.com/api/v1/board/sessions/00000d1b-0000-4000-8000-d0c500000000/history" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "session": {
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "label": "<label>",
    "kind": "<kind>",
    "branch": "<branch>",
    "worktree": "<worktree>",
    "started_at": "<started_at>",
    "last_seen_at": "<last_seen_at>",
    "finished_at": "<finished_at>",
    "is_live": true,
    "ticket_ids": [
      "fbc29b2e-0000-4000-8000-d0c5000000fb"
    ],
    "in_progress": -9007199254740991,
    "in_review": -9007199254740991,
    "testing": -9007199254740991,
    "todo": -9007199254740991,
    "done": -9007199254740991,
    "goal": "<goal>",
    "filed_count": 1,
    "finished_count": 1,
    "history_count": 1
  },
  "history": [
    {
      "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
      "title": "Morning class",
      "col": "<col>",
      "type": "<type>",
      "prio": "<prio>",
      "section": "<section>",
      "initiative": "<initiative>",
      "reopen_count": 1,
      "linkage": [
        "<linkage>"
      ],
      "verbs": [
        "<verb>"
      ],
      "first_event_at": "<first_event_at>",
      "last_event_at": "<last_event_at>",
      "view_count": 1
    }
  ],
  "filed_count": 1,
  "finished_count": 1,
  "sections": [
    "<section>"
  ],
  "initiatives": [
    "<initiative>"
  ],
  "first_activity_at": "<first_activity_at>"
}

What should this thread do next?

R21-R23. Answered the way a developer would answer it: finish what you are already holding; else do what you were named on; else take from the AI pool; else take the top of the pick-next ranking. Both pool and ranking come from the same candidate view, so the dependency gate and the founder ordering always apply. Returns a real null with a reason rather than inventing something to do.

Parameters

idstringpathrequired
max_in_flightintegerqueryoptional

R23: how many tickets one thread may hold in Doing/Review/Testing at once. Default 1. A DEFAULT, not a lock: at the limit this returns no ticket and says why, but nothing stops a thread taking one anyway.

Response, 200

session_idstringrequired
ticketobjectrequired

The ticket to pick up, or null with a reason saying why not.

50 child fields
idstringrequired
sourcestringrequired
pr_numbernumber, nullablerequired
branchstring, nullablerequired
sectionstring, nullablerequired
colstring, nullablerequired

Resolved column: the founder override (founder_col) if set, else the synced col.

founder_colstring, nullablerequired

The founder column override, or null when the item is not founder-pinned.

pr_statestring, nullablerequired
typestringrequired

Resolved type: type_override if set, else the synced type.

priostringrequired

Resolved priority: prio_override if set, else the synced prio.

titlestring, nullablerequired

Resolved title: display_title ?? title ?? raw_title.

raw_titlestring, nullablerequired
summarystring, nullablerequired

The plain-terms "In short" TL;DR every ticket opens with.

after_donestring, nullablerequired

Founder-action rows: "What happens after you tick it", the post-done "Next:" card.

whystring, nullablerequired
what_it_doesstring, nullablerequired
user_storystring, nullablerequired
epic_idstring, nullablerequired
when_tsstring, nullablerequired

ISO date (YYYY-MM-DD) the work landed / was raised.

content_sourcestring, nullablerequired
synced_atstringrequired

ISO timestamp of the last sync write.

assigneestring, nullablerequired
claimed_atstring, nullablerequired
session_idstring, nullablerequired
depends_onarray of stringrequired

R16: ticket ids this one is waiting on. Empty = not blocked.

labelsarray of stringrequired
notesstring, nullablerequired
founder_ordnumber, nullablerequired
created_tsstring, nullablerequired

The time dimension: when the item was born (PR createdAt / FA added date). Null when the source has no date.

updated_tsstring, nullablerequired

When the item last moved at its source (PR updatedAt). Null when unknown.

initiativestring, nullablerequired

The ONE initiative/vertical this ticket serves: gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform. 'platform' is the honest cross-cutting none. Null = never classified. (Read kept permissive: the vocabulary may grow without a migration.)

merchantstring, nullablerequired

Canonical store slug when the ticket is about ONE real store (busy-beans, tc60, flour-bloom, larkfield, …). Null = no single store / never classified.

addonsarray of stringrequired

Add-on slugs from the real addons catalog (order_collect, bookings, gym_management, own_app, retention_engine) plus family-level 'moments'. Null = never classified; [] = classified as none, the distinction is deliberate and kept on the wire.

parent_idstring, nullablerequired

R17: the ticket this is a sub-task of, or null when it stands alone.

human_contextstring, nullablerequired

R14: the SHORT plain-English line, AS AUTHORED. Null when nobody has written one. getBoardTicket returns a derived fallback in its own top-level human_context field; this column stays honest about whether a human actually wrote it.

assignee_kindenumrequired

'human' with an assignee = a person owns it. 'ai' with a session_id = THAT thread owns it. 'ai' with no session_id = the pool, any thread may pick it up. null = unassigned.

humanai
reopened_atstring, nullablerequired

R9: when it last came back out of Done, or null if it never has.

reopen_countintegerrequired

R9: how many times it has come back out of Done. 0 for most tickets.

capture_textstring, nullablerequired

Founder quick capture (18 Aug 2026): his VERBATIM words at capture time (source 'founder-capture'). Kept forever, even after triage fills the brief, his words are the record. Null on every other source.

subtask_idsarray of stringrequired

R17: ids of the tickets whose parent_id is this one. Computed on read, never written.

children_doneintegerrequired

Honest per-ticket progress (19 Aug 2026): how many of subtask_ids sit in a RESOLVED column of 'done', the DONE half of the card's "3 of 8". The TOTAL is subtask_ids.length, so both halves have one source and cannot disagree. 0 when there are no children. Never a percentage.

created_by_founderbooleanrequired

True when the FOUNDER filed this row himself, across all three intake paths: quick capture (source='founder-capture'), the ⊕ New-ticket sheet (source='manual' AND content_source='founder'), or a bug he raised that was later filed against a board_inbox row with asking_session_id NULL. Computed by the one server-side predicate (isFounderCreated in src/lib/board/resolve.ts), never re-derived from the id prefix, which is wrong on the live board.

founder_ownedbooleanrequired

True when the row is HIS: everything created_by_founder covers, PLUS his own errand list (source='founder-action', the FA items). Strictly wider than created_by_founder and never narrower. Two fields because the questions differ, provenance decides whether a trust-loop push fires (only for what he actually filed), while the board's "Yours" view decides what he sees. Computed by isFounderOwned in src/lib/board/resolve.ts.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

Computed, never written. working_now = a LIVE session (silent < 45 min, the board_liveness_window) holds it by session_id OR assignee=label OR branch=branch, the ONLY state a screen may present as being worked. started_nobody_on_it = in a started column and NO session has ever held it. abandoned_was_held = a session did hold it and that thread is finished or long silent. not_applicable = not in a started column at all (To-do, Done, Blocked, Triage have no liveness question to answer).

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
worked_bystring, nullablerequired

The live developer's name when work_state is working_now ("Tomas Ashworth"), else null. The ONLY field a card may use to claim somebody is working, there is no client-side fallback, by design.

was_worked_bystring, nullablerequired

The name of the developer who HAD it, when nobody has it now. Null while someone is on it, and null when nobody ever was, so an abandoned card can say "Hugh Draper stopped" instead of going anonymous.

went_quiet_atstring, nullablerequired

When that developer was last seen, so a card can say "quiet since 09:14".

released_atstring, nullablerequired

When this ticket was last taken off a dead thread and handed back. claimed_at is PRESERVED alongside it, so "started" and "given back" are both readable.

ai_contextobjectoptional

R15, DETAIL ONLY. Present on getBoardTicket, ABSENT on a list row and on a mutation echo: rendering it for 123 rows would be megabytes on every board refresh. A client keeps the copy it loaded when a fresher row arrives without one, rather than blanking the brief mid-session.

3 child fields
markdownstringrequired

The whole brief as one paste-and-go document. This is what an agent is handed.

sectionsarray of objectrequired
4 child fields
keystringrequired

Stable and machine-readable: what_needs_doing, at_a_glance, pr, history, ...

titlestringrequired
bodystring, nullablerequired
itemsarray of stringrequired

List-shaped sections (dependencies, sub-tasks, history) come back as items.

generated_atstringrequired

When the server composed it, so a stale brief is visible as stale.

sourceenumrequired

Which bucket it came from. 'assigned' = this thread was named on it. 'ai_pool' = marked for AI, no thread named. 'unclaimed' = the ordinary pick-next ranking. 'triage' = a founder capture waiting to be SHAPED, not claimed: read his words (capture_text + attachments), then call triageBoardTicket with the full brief. A thread should know whether it was ASKED.

assignedai_poolunclaimedtriage
reasonenumrequired
okneeds_triageat_wip_limitnothing_to_do
in_flightarray of objectrequired

What this thread already holds in Doing, Review or Testing.

8 child fields
idstringrequired
titlestring, nullablerequired
colstring, nullablerequired
typestringrequired
priostringrequired
reopen_countintegerrequired

So a link can show the reopened badge without a second fetch.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

So a linked sub-task or dependency can show that somebody is on it without a second fetch. Same computed field, same one definition.

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
max_in_flightintegerrequired
curl -G "https://www.membber.com/api/v1/board/sessions/00000d1b-0000-4000-8000-d0c500000000/next" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "session_id": "630ddf64-0000-4000-8000-d0c500000063",
  "ticket": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "source": "<source>",
    "pr_number": 1,
    "branch": "<branch>",
    "section": "<section>",
    "col": "<col>",
    "founder_col": "<founder_col>",
    "pr_state": "<pr_state>",
    "type": "<type>",
    "prio": "<prio>",
    "title": "Morning class",
    "raw_title": "<raw_title>",
    "summary": "<summary>",
    "after_done": "<after_done>",
    "why": "<why>",
    "what_it_does": "<what_it_does>",
    "user_story": "<user_story>",
    "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
    "when_ts": "<when_ts>",
    "content_source": "<content_source>",
    "synced_at": "<synced_at>",
    "assignee": "<assignee>",
    "claimed_at": "<claimed_at>",
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "depends_on": [
      "<depends_on>"
    ],
    "labels": [
      "<label>"
    ],
    "notes": "Added at the front desk",
    "founder_ord": 1,
    "created_ts": "<created_ts>",
    "updated_ts": "<updated_ts>",
    "initiative": "<initiative>",
    "merchant": "<merchant>",
    "addons": [
      "<addon>"
    ],
    "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
    "human_context": "<human_context>",
    "assignee_kind": "human",
    "reopened_at": "<reopened_at>",
    "reopen_count": 1,
    "capture_text": "<capture_text>",
    "subtask_ids": [
      "e8d74bd5-0000-4000-8000-d0c5000000e8"
    ],
    "children_done": -9007199254740991,
    "created_by_founder": true,
    "founder_owned": true,
    "lifecycle": "awaiting_triage",
    "work_state": "working_now",
    "worked_by": "<worked_by>",
    "was_worked_by": "<was_worked_by>",
    "went_quiet_at": "<went_quiet_at>",
    "released_at": "<released_at>",
    "ai_context": {
      "markdown": "<markdown>",
      "sections": [
        {
          "key": "<key>",
          "title": "Morning class",
          "body": "<body>",
          "items": [
            "<item>"
          ]
        }
      ],
      "generated_at": "<generated_at>"
    }
  },
  "source": "assigned",
  "reason": "ok",
  "in_flight": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "title": "Morning class",
      "col": "<col>",
      "type": "<type>",
      "prio": "<prio>",
      "reopen_count": 1,
      "lifecycle": "awaiting_triage",
      "work_state": "working_now"
    }
  ],
  "max_in_flight": -9007199254740991
}

A developer's recently-viewed tickets (board_ticket_views by actor).

Parameters

idstringpathrequired

Response, 200

viewsarray of objectrequired

Tickets this developer has opened, most recent first. Titles resolve client-side from the loaded board.

3 child fields
ticket_idstringrequired
last_viewed_atstringrequired
view_countintegerrequired
curl -G "https://www.membber.com/api/v1/board/sessions/00000d1b-0000-4000-8000-d0c500000000/views" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "views": [
    {
      "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
      "last_viewed_at": "<last_viewed_at>",
      "view_count": 1
    }
  ]
}

A thread says it is alive. Creates it on first contact, revives it if it had finished.

R22: the founder needs to see threads working as it happens, so a thread checks in on startup and then periodically. This is also the ONLY way a session comes into existence: every other route refuses to write a session_id it has never seen, so a ticket can never be owned by a thread that does not exist.

Request body

session_idstringrequired

The thread's stable id. The same thread must send the same id every time.

labelstringoptional

A human-readable name for the thread, e.g. "board redesign, API". Never blanked once known.

worktreestringoptional

The worktree it is working in.

branchstringoptional

The branch it is working on.

kindenumoptional

Defaults to 'ai'.

aihuman
goalstringoptional

The mission this thread carries, in plain words. Coalesce-preserving like label: never blanked once known.

Response, 200

sessionobjectrequired
19 child fields
session_idstringrequired
labelstring, nullablerequired

The human-readable name of the thread, or null if it never sent one.

kindstringrequired

'ai' or 'human'.

branchstring, nullablerequired
worktreestring, nullablerequired
started_atstringrequired
last_seen_atstringrequired
finished_atstring, nullablerequired
is_livebooleanrequired

R24: not finished AND seen inside the staleness window. This is what "a live session" means.

ticket_idsarray of stringrequired
in_progressintegerrequired

R25: how many tickets this thread holds in Doing.

in_reviewintegerrequired
testingintegerrequired
todointegerrequired
doneintegerrequired
goalstring, nullablerequired

The mission this thread carries, in the words it was given ("fix click and collect bugs"). From the kit's `hello --goal`, or the spawn request's instructions for a phone-spawned thread. Null when never stated, a client renders nothing, never an invented mission.

filed_countintegerrequired

Tickets this developer FILED, counted across every real linkage (content_source `thread:<label>` + activity `filed` rows), not just live session_id ownership. The zero-tickets fix (17 Aug 2026): 28 of 38 developers read zero under the session_id-only join while 27 of them had real history on other linkages.

finished_countintegerrequired

Linked tickets now sitting in Done, across every linkage.

history_countintegerrequired

Distinct tickets with ANY real linkage to this developer (held / filed / touched / branch). Views excluded.

curl -X POST "https://www.membber.com/api/v1/board/sessions/heartbeat" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "630ddf64-0000-4000-8000-d0c500000063"
  }'
Response, 200
{
  "session": {
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "label": "<label>",
    "kind": "<kind>",
    "branch": "<branch>",
    "worktree": "<worktree>",
    "started_at": "<started_at>",
    "last_seen_at": "<last_seen_at>",
    "finished_at": "<finished_at>",
    "is_live": true,
    "ticket_ids": [
      "fbc29b2e-0000-4000-8000-d0c5000000fb"
    ],
    "in_progress": -9007199254740991,
    "in_review": -9007199254740991,
    "testing": -9007199254740991,
    "todo": -9007199254740991,
    "done": -9007199254740991,
    "goal": "<goal>",
    "filed_count": 1,
    "finished_count": 1,
    "history_count": 1
  }
}

Recent New-developer requests and their live status.

auth operator:boardop listBoardSpawnRequestsrate limitederrors

Response, 200

requestsarray of objectrequired
12 child fields
idstringrequired
created_atstringrequired
requested_bystringrequired
sectionstring, nullablerequired
ticket_idstring, nullablerequired
instructionsstring, nullablerequired
statusstringrequired

pending | launching | launched | failed | cancelled

claimed_atstring, nullablerequired
session_idstring, nullablerequired
worktreestring, nullablerequired
branchstring, nullablerequired
errorstring, nullablerequired
curl -G "https://www.membber.com/api/v1/board/spawn" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "requests": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "created_at": "<created_at>",
      "requested_by": "<requested_by>",
      "section": "<section>",
      "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
      "instructions": "<instructions>",
      "status": "<status>",
      "claimed_at": "<claimed_at>",
      "session_id": "630ddf64-0000-4000-8000-d0c500000063",
      "worktree": "<worktree>",
      "branch": "<branch>",
      "error": "<error>"
    }
  ]
}

Queue a new AI developer session from the founder's phone.

Inserts a pending row in board_spawn_requests (service-role-only table). The launcher daemon on the founder's Mac, installed by HIM (FA055); the phone cannot execute anything directly, claims it atomically, creates a fresh worktree, and starts a visible Claude session that registers on the roster via the thread kit. Capped at 3 launches per rolling hour on the Mac side.

Request body

sectionstringoptional

Section the new developer should work (e.g. hq, payments).

ticket_idstringoptional

A specific ticket to put the developer on.

instructionsstringoptional

The founder's brief, in his words.

client_request_idstringoptional

Per-tap idempotency key: a retry returns the first request instead of queueing a twin.

Response, 200

requestobjectrequired
12 child fields
idstringrequired
created_atstringrequired
requested_bystringrequired
sectionstring, nullablerequired
ticket_idstring, nullablerequired
instructionsstring, nullablerequired
statusstringrequired

pending | launching | launched | failed | cancelled

claimed_atstring, nullablerequired
session_idstring, nullablerequired
worktreestring, nullablerequired
branchstring, nullablerequired
errorstring, nullablerequired
curl -X POST "https://www.membber.com/api/v1/board/spawn" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "section": "<section>",
    "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
    "instructions": "<instructions>",
    "client_request_id": "8e99e5bf-0000-4000-8000-d0c50000008e"
  }'
Response, 200
{
  "request": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "created_at": "<created_at>",
    "requested_by": "<requested_by>",
    "section": "<section>",
    "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
    "instructions": "<instructions>",
    "status": "<status>",
    "claimed_at": "<claimed_at>",
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "worktree": "<worktree>",
    "branch": "<branch>",
    "error": "<error>"
  }
}

List board tickets (filterable + keyset-paginated), founder overlay resolved.

Parameters

colstringqueryoptional

Filter to a resolved column (e.g. todo, doing, review, done, blocked).

typestringqueryoptional

Filter to a resolved type (bug, feature, task).

sectionstringqueryoptional

Filter to a section (payments, verticals, business, customer, platform).

initiativestringqueryoptional

Board labels (17 Aug 2026): filter to one initiative, gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform ('platform' = cross-cutting work).

merchantstringqueryoptional

Board labels: filter to tickets about one store, by canonical slug (busy-beans, tc60, flour-bloom, …).

addonstringqueryoptional

Board labels: filter to tickets touching one add-on, order_collect | bookings | gym_management | own_app | moments | retention_engine. Matches rows whose addons array contains it.

areastringqueryoptional

Focus-first filter (18 Aug 2026): ONE area slug, a named predicate from board_label_vocabulary expanding server-side to an OR-union over sections / initiatives / addons (e.g. gym = initiative gym OR addon gym_management; 'everything-else' = the complement no other area claims). The list includes matching done history; label_facets.areas carries the open-only counts. Vocabulary kept permissive: a new area is one board_label_vocabulary row, no migration.

qstringqueryoptional

Free-text match against title, raw title, why, id, branch and assignee.

session_idstringqueryoptional

R24: only the tickets one thread is holding. Pair with GET /sessions to build the filter.

assignee_kindenumqueryoptional

R4-R7: 'human' = owned by a person, 'ai' = owned by AI (a named thread OR the pool), 'unassigned' = nobody has it, which R7 calls "not being picked up yet".

humanaiunassigned
parent_idstringqueryoptional

R17: only the sub-tasks of one ticket.

mineenumqueryoptional

"Yours" (19 Aug 2026): pass mine=1 for only the tickets that are HIS, what he filed (quick capture, the ⊕ New-ticket sheet, or a bug he raised that was later filed) AND his own errand list. Same predicate as the founder_owned field on every row, which is wider than created_by_founder and never narrower. Absent = the whole board.

1true
work_stateenumqueryoptional

The founder's ask (19 Aug 2026): work_state=working_now is "what is being worked on right now"; work_state=abandoned_was_held makes stalled work findable. Combine with `prio` for "high priority work being worked right now" in one tap.

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
priostringqueryoptional

One priority, or several comma-separated ("critical,high,urgent"). A LIST because `urgent` is a legacy synonym still live on real rows and board_next_candidates already ranks critical and high/urgent separately, a single-value filter silently misses rows, the exact omission that made a CRITICAL founder capture sort as normal before 19 Aug.

limitintegerqueryoptional

Page size (default 50, max 200).

cursorstringqueryoptional

Opaque keyset cursor from a previous page.

Response, 200

ticketsarray of objectrequired
50 child fields
idstringrequired
sourcestringrequired
pr_numbernumber, nullablerequired
branchstring, nullablerequired
sectionstring, nullablerequired
colstring, nullablerequired

Resolved column: the founder override (founder_col) if set, else the synced col.

founder_colstring, nullablerequired

The founder column override, or null when the item is not founder-pinned.

pr_statestring, nullablerequired
typestringrequired

Resolved type: type_override if set, else the synced type.

priostringrequired

Resolved priority: prio_override if set, else the synced prio.

titlestring, nullablerequired

Resolved title: display_title ?? title ?? raw_title.

raw_titlestring, nullablerequired
summarystring, nullablerequired

The plain-terms "In short" TL;DR every ticket opens with.

after_donestring, nullablerequired

Founder-action rows: "What happens after you tick it", the post-done "Next:" card.

whystring, nullablerequired
what_it_doesstring, nullablerequired
user_storystring, nullablerequired
epic_idstring, nullablerequired
when_tsstring, nullablerequired

ISO date (YYYY-MM-DD) the work landed / was raised.

content_sourcestring, nullablerequired
synced_atstringrequired

ISO timestamp of the last sync write.

assigneestring, nullablerequired
claimed_atstring, nullablerequired
session_idstring, nullablerequired
depends_onarray of stringrequired

R16: ticket ids this one is waiting on. Empty = not blocked.

labelsarray of stringrequired
notesstring, nullablerequired
founder_ordnumber, nullablerequired
created_tsstring, nullablerequired

The time dimension: when the item was born (PR createdAt / FA added date). Null when the source has no date.

updated_tsstring, nullablerequired

When the item last moved at its source (PR updatedAt). Null when unknown.

initiativestring, nullablerequired

The ONE initiative/vertical this ticket serves: gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform. 'platform' is the honest cross-cutting none. Null = never classified. (Read kept permissive: the vocabulary may grow without a migration.)

merchantstring, nullablerequired

Canonical store slug when the ticket is about ONE real store (busy-beans, tc60, flour-bloom, larkfield, …). Null = no single store / never classified.

addonsarray of stringrequired

Add-on slugs from the real addons catalog (order_collect, bookings, gym_management, own_app, retention_engine) plus family-level 'moments'. Null = never classified; [] = classified as none, the distinction is deliberate and kept on the wire.

parent_idstring, nullablerequired

R17: the ticket this is a sub-task of, or null when it stands alone.

human_contextstring, nullablerequired

R14: the SHORT plain-English line, AS AUTHORED. Null when nobody has written one. getBoardTicket returns a derived fallback in its own top-level human_context field; this column stays honest about whether a human actually wrote it.

assignee_kindenumrequired

'human' with an assignee = a person owns it. 'ai' with a session_id = THAT thread owns it. 'ai' with no session_id = the pool, any thread may pick it up. null = unassigned.

humanai
reopened_atstring, nullablerequired

R9: when it last came back out of Done, or null if it never has.

reopen_countintegerrequired

R9: how many times it has come back out of Done. 0 for most tickets.

capture_textstring, nullablerequired

Founder quick capture (18 Aug 2026): his VERBATIM words at capture time (source 'founder-capture'). Kept forever, even after triage fills the brief, his words are the record. Null on every other source.

subtask_idsarray of stringrequired

R17: ids of the tickets whose parent_id is this one. Computed on read, never written.

children_doneintegerrequired

Honest per-ticket progress (19 Aug 2026): how many of subtask_ids sit in a RESOLVED column of 'done', the DONE half of the card's "3 of 8". The TOTAL is subtask_ids.length, so both halves have one source and cannot disagree. 0 when there are no children. Never a percentage.

created_by_founderbooleanrequired

True when the FOUNDER filed this row himself, across all three intake paths: quick capture (source='founder-capture'), the ⊕ New-ticket sheet (source='manual' AND content_source='founder'), or a bug he raised that was later filed against a board_inbox row with asking_session_id NULL. Computed by the one server-side predicate (isFounderCreated in src/lib/board/resolve.ts), never re-derived from the id prefix, which is wrong on the live board.

founder_ownedbooleanrequired

True when the row is HIS: everything created_by_founder covers, PLUS his own errand list (source='founder-action', the FA items). Strictly wider than created_by_founder and never narrower. Two fields because the questions differ, provenance decides whether a trust-loop push fires (only for what he actually filed), while the board's "Yours" view decides what he sees. Computed by isFounderOwned in src/lib/board/resolve.ts.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

Computed, never written. working_now = a LIVE session (silent < 45 min, the board_liveness_window) holds it by session_id OR assignee=label OR branch=branch, the ONLY state a screen may present as being worked. started_nobody_on_it = in a started column and NO session has ever held it. abandoned_was_held = a session did hold it and that thread is finished or long silent. not_applicable = not in a started column at all (To-do, Done, Blocked, Triage have no liveness question to answer).

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
worked_bystring, nullablerequired

The live developer's name when work_state is working_now ("Tomas Ashworth"), else null. The ONLY field a card may use to claim somebody is working, there is no client-side fallback, by design.

was_worked_bystring, nullablerequired

The name of the developer who HAD it, when nobody has it now. Null while someone is on it, and null when nobody ever was, so an abandoned card can say "Hugh Draper stopped" instead of going anonymous.

went_quiet_atstring, nullablerequired

When that developer was last seen, so a card can say "quiet since 09:14".

released_atstring, nullablerequired

When this ticket was last taken off a dead thread and handed back. claimed_at is PRESERVED alongside it, so "started" and "given back" are both readable.

ai_contextobjectoptional

R15, DETAIL ONLY. Present on getBoardTicket, ABSENT on a list row and on a mutation echo: rendering it for 123 rows would be megabytes on every board refresh. A client keeps the copy it loaded when a fresher row arrives without one, rather than blanking the brief mid-session.

3 child fields
markdownstringrequired

The whole brief as one paste-and-go document. This is what an agent is handed.

sectionsarray of objectrequired
4 child fields
keystringrequired

Stable and machine-readable: what_needs_doing, at_a_glance, pr, history, ...

titlestringrequired
bodystring, nullablerequired
itemsarray of stringrequired

List-shaped sections (dependencies, sub-tasks, history) come back as items.

generated_atstringrequired

When the server composed it, so a stale brief is visible as stale.

next_cursorstring, nullablerequired

Cursor for the next page, or null when the list is exhausted.

label_facetsobjectrequired

Whole-board label inventory + vocabulary, computed over every resolved row BEFORE filters or pagination, so a filter UI can offer every label the board carries with its true count, never just the values inside one fetched page. Excludes source=founder-action rows (the HQ board hides them). Every chip number on the HQ filter panel comes from this one server computation.

12 child fields
totalintegerrequired

OPEN (col ≠ done) non-founder-action tickets. Counting regime (18 Aug 2026): every label/area count here is open work only, while the LIST a filter produces still includes matching done history, "counts = open only, list = everything matching".

initiativesobjectrequired

initiative slug → open-ticket count.

merchantsobjectrequired

merchant slug → open-ticket count.

addonsobjectrequired

add-on slug → open tickets whose addons array contains it.

areasobjectrequired

area slug → open-ticket count. Areas are named predicates over the existing columns and OVERLAP by design (a gym booking fix sits in two areas), per-chip counts, never a partition.

stagesobjectrequired

resolved column → ticket count, done INCLUDED under its own key (the Stage facet is where history is deliberately reachable; its Done number is the real history size).

sessionsobjectrequired

session id → open tickets that thread holds.

poolintegerrequired

Open tickets marked for AI with no thread holding them yet.

unassignedintegerrequired

Open tickets nothing is holding at all.

mineintegerrequired

Open tickets that are HIS, the count behind the "Yours" facet, and the ONLY count here that includes his errand list. Every other number on this object measures what the FLEET is doing and deliberately skips founder-action rows; this one cannot, or the chip would say a different number from the list it opens. Matches founder_owned.

work_statesobjectrequired

work_state → count, over OPEN work only, the same regime as every other count here. This is what makes "Working now" and "Abandoned" real chips with real numbers rather than labels the app has to guess at.

vocabularyobjectrequired

The label vocabulary (board_label_vocabulary): names, order and area predicates. A new initiative/merchant/area is ONE row there and appears here on the next response.

4 child fields
initiativesarray of objectrequired
4 child fields
slugstringrequired
namestringrequired

The display name a chip renders, server-named so the vocabulary grows with zero app changes.

ordintegerrequired

Canonical sort order within its kind.

termsobjectoptional

Areas only: the OR-union predicate, mirrored client-side for the instant local match.

4 child fields
sectionsarray of stringrequired
initiativesarray of stringrequired
addonsarray of stringrequired
complementbooleanrequired

true = "Everything else": matches what no other area claims.

addonsarray of objectrequired
4 child fields
slugstringrequired
namestringrequired

The display name a chip renders, server-named so the vocabulary grows with zero app changes.

ordintegerrequired

Canonical sort order within its kind.

termsobjectoptional

Areas only: the OR-union predicate, mirrored client-side for the instant local match.

4 child fields
sectionsarray of stringrequired
initiativesarray of stringrequired
addonsarray of stringrequired
complementbooleanrequired

true = "Everything else": matches what no other area claims.

merchantsarray of objectrequired
4 child fields
slugstringrequired
namestringrequired

The display name a chip renders, server-named so the vocabulary grows with zero app changes.

ordintegerrequired

Canonical sort order within its kind.

termsobjectoptional

Areas only: the OR-union predicate, mirrored client-side for the instant local match.

4 child fields
sectionsarray of stringrequired
initiativesarray of stringrequired
addonsarray of stringrequired
complementbooleanrequired

true = "Everything else": matches what no other area claims.

areasarray of objectrequired
4 child fields
slugstringrequired
namestringrequired

The display name a chip renders, server-named so the vocabulary grows with zero app changes.

ordintegerrequired

Canonical sort order within its kind.

termsobjectoptional

Areas only: the OR-union predicate, mirrored client-side for the instant local match.

4 child fields
sectionsarray of stringrequired
initiativesarray of stringrequired
addonsarray of stringrequired
complementbooleanrequired

true = "Everything else": matches what no other area claims.

curl -G "https://www.membber.com/api/v1/board/tickets" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "tickets": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "source": "<source>",
      "pr_number": 1,
      "branch": "<branch>",
      "section": "<section>",
      "col": "<col>",
      "founder_col": "<founder_col>",
      "pr_state": "<pr_state>",
      "type": "<type>",
      "prio": "<prio>",
      "title": "Morning class",
      "raw_title": "<raw_title>",
      "summary": "<summary>",
      "after_done": "<after_done>",
      "why": "<why>",
      "what_it_does": "<what_it_does>",
      "user_story": "<user_story>",
      "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
      "when_ts": "<when_ts>",
      "content_source": "<content_source>",
      "synced_at": "<synced_at>",
      "assignee": "<assignee>",
      "claimed_at": "<claimed_at>",
      "session_id": "630ddf64-0000-4000-8000-d0c500000063",
      "depends_on": [
        "<depends_on>"
      ],
      "labels": [
        "<label>"
      ],
      "notes": "Added at the front desk",
      "founder_ord": 1,
      "created_ts": "<created_ts>",
      "updated_ts": "<updated_ts>",
      "initiative": "<initiative>",
      "merchant": "<merchant>",
      "addons": [
        "<addon>"
      ],
      "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
      "human_context": "<human_context>",
      "assignee_kind": "human",
      "reopened_at": "<reopened_at>",
      "reopen_count": 1,
      "capture_text": "<capture_text>",
      "subtask_ids": [
        "e8d74bd5-0000-4000-8000-d0c5000000e8"
      ],
      "children_done": -9007199254740991,
      "created_by_founder": true,
      "founder_owned": true,
      "lifecycle": "awaiting_triage",
      "work_state": "working_now",
      "worked_by": "<worked_by>",
      "was_worked_by": "<was_worked_by>",
      "went_quiet_at": "<went_quiet_at>",
      "released_at": "<released_at>",
      "ai_context": {
        "markdown": "<markdown>",
        "sections": [
          {
            "key": "<key>",
            "title": "Morning class",
            "body": "<body>",
            "items": [
              null
            ]
          }
        ],
        "generated_at": "<generated_at>"
      }
    }
  ],
  "next_cursor": "<next_cursor>",
  "label_facets": {
    "total": -9007199254740991,
    "initiatives": {},
    "merchants": {},
    "addons": {},
    "areas": {},
    "stages": {},
    "sessions": {},
    "pool": -9007199254740991,
    "unassigned": -9007199254740991,
    "mine": -9007199254740991,
    "work_states": {},
    "vocabulary": {
      "initiatives": [
        {
          "slug": "<slug>",
          "name": "Example name",
          "ord": -9007199254740991,
          "terms": {
            "sections": [
              null
            ],
            "initiatives": [
              null
            ],
            "addons": [
              null
            ],
            "complement": true
          }
        }
      ],
      "addons": [
        {
          "slug": "<slug>",
          "name": "Example name",
          "ord": -9007199254740991,
          "terms": {
            "sections": [
              null
            ],
            "initiatives": [
              null
            ],
            "addons": [
              null
            ],
            "complement": true
          }
        }
      ],
      "merchants": [
        {
          "slug": "<slug>",
          "name": "Example name",
          "ord": -9007199254740991,
          "terms": {
            "sections": [
              null
            ],
            "initiatives": [
              null
            ],
            "addons": [
              null
            ],
            "complement": true
          }
        }
      ],
      "areas": [
        {
          "slug": "<slug>",
          "name": "Example name",
          "ord": -9007199254740991,
          "terms": {
            "sections": [
              null
            ],
            "initiatives": [
              null
            ],
            "addons": [
              null
            ],
            "complement": true
          }
        }
      ]
    }
  }
}

File a new board ticket from the founder's phone (the ⊕ FAB).

Inserts source='manual', content_source='founder', col='todo', id `M-<slug-of-title>`. Idempotent on the slug: re-filing the same title refreshes that ticket's content fields and never touches col/assignee/session, a ticket someone already holds is never moved. Same NO-BLANKS validation as scripts/board-thread.mjs file-ticket.

Request body

titlestringrequired

The ticket title, becomes the M-<slug> id.

sectionenumrequired

Which initiative this belongs to (same set file-ticket accepts).

customerbusinesspaymentsverticalsplatformhq
typeenumrequired

R2: task | bug | feature | epic | chore.

taskbugfeatureepicchore
priorityenumrequired

The board priority scale.

criticalhighnormallow
whystringrequired

NO-BLANKS law: why this exists, in real words (≥20 chars, mandatory).

whatstringrequired

NO-BLANKS law: what the change does (≥20 chars, mandatory). Stored as what_it_does.

Response, 200

ticketobjectrequired
50 child fields
idstringrequired
sourcestringrequired
pr_numbernumber, nullablerequired
branchstring, nullablerequired
sectionstring, nullablerequired
colstring, nullablerequired

Resolved column: the founder override (founder_col) if set, else the synced col.

founder_colstring, nullablerequired

The founder column override, or null when the item is not founder-pinned.

pr_statestring, nullablerequired
typestringrequired

Resolved type: type_override if set, else the synced type.

priostringrequired

Resolved priority: prio_override if set, else the synced prio.

titlestring, nullablerequired

Resolved title: display_title ?? title ?? raw_title.

raw_titlestring, nullablerequired
summarystring, nullablerequired

The plain-terms "In short" TL;DR every ticket opens with.

after_donestring, nullablerequired

Founder-action rows: "What happens after you tick it", the post-done "Next:" card.

whystring, nullablerequired
what_it_doesstring, nullablerequired
user_storystring, nullablerequired
epic_idstring, nullablerequired
when_tsstring, nullablerequired

ISO date (YYYY-MM-DD) the work landed / was raised.

content_sourcestring, nullablerequired
synced_atstringrequired

ISO timestamp of the last sync write.

assigneestring, nullablerequired
claimed_atstring, nullablerequired
session_idstring, nullablerequired
depends_onarray of stringrequired

R16: ticket ids this one is waiting on. Empty = not blocked.

labelsarray of stringrequired
notesstring, nullablerequired
founder_ordnumber, nullablerequired
created_tsstring, nullablerequired

The time dimension: when the item was born (PR createdAt / FA added date). Null when the source has no date.

updated_tsstring, nullablerequired

When the item last moved at its source (PR updatedAt). Null when unknown.

initiativestring, nullablerequired

The ONE initiative/vertical this ticket serves: gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform. 'platform' is the honest cross-cutting none. Null = never classified. (Read kept permissive: the vocabulary may grow without a migration.)

merchantstring, nullablerequired

Canonical store slug when the ticket is about ONE real store (busy-beans, tc60, flour-bloom, larkfield, …). Null = no single store / never classified.

addonsarray of stringrequired

Add-on slugs from the real addons catalog (order_collect, bookings, gym_management, own_app, retention_engine) plus family-level 'moments'. Null = never classified; [] = classified as none, the distinction is deliberate and kept on the wire.

parent_idstring, nullablerequired

R17: the ticket this is a sub-task of, or null when it stands alone.

human_contextstring, nullablerequired

R14: the SHORT plain-English line, AS AUTHORED. Null when nobody has written one. getBoardTicket returns a derived fallback in its own top-level human_context field; this column stays honest about whether a human actually wrote it.

assignee_kindenumrequired

'human' with an assignee = a person owns it. 'ai' with a session_id = THAT thread owns it. 'ai' with no session_id = the pool, any thread may pick it up. null = unassigned.

humanai
reopened_atstring, nullablerequired

R9: when it last came back out of Done, or null if it never has.

reopen_countintegerrequired

R9: how many times it has come back out of Done. 0 for most tickets.

capture_textstring, nullablerequired

Founder quick capture (18 Aug 2026): his VERBATIM words at capture time (source 'founder-capture'). Kept forever, even after triage fills the brief, his words are the record. Null on every other source.

subtask_idsarray of stringrequired

R17: ids of the tickets whose parent_id is this one. Computed on read, never written.

children_doneintegerrequired

Honest per-ticket progress (19 Aug 2026): how many of subtask_ids sit in a RESOLVED column of 'done', the DONE half of the card's "3 of 8". The TOTAL is subtask_ids.length, so both halves have one source and cannot disagree. 0 when there are no children. Never a percentage.

created_by_founderbooleanrequired

True when the FOUNDER filed this row himself, across all three intake paths: quick capture (source='founder-capture'), the ⊕ New-ticket sheet (source='manual' AND content_source='founder'), or a bug he raised that was later filed against a board_inbox row with asking_session_id NULL. Computed by the one server-side predicate (isFounderCreated in src/lib/board/resolve.ts), never re-derived from the id prefix, which is wrong on the live board.

founder_ownedbooleanrequired

True when the row is HIS: everything created_by_founder covers, PLUS his own errand list (source='founder-action', the FA items). Strictly wider than created_by_founder and never narrower. Two fields because the questions differ, provenance decides whether a trust-loop push fires (only for what he actually filed), while the board's "Yours" view decides what he sees. Computed by isFounderOwned in src/lib/board/resolve.ts.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

Computed, never written. working_now = a LIVE session (silent < 45 min, the board_liveness_window) holds it by session_id OR assignee=label OR branch=branch, the ONLY state a screen may present as being worked. started_nobody_on_it = in a started column and NO session has ever held it. abandoned_was_held = a session did hold it and that thread is finished or long silent. not_applicable = not in a started column at all (To-do, Done, Blocked, Triage have no liveness question to answer).

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
worked_bystring, nullablerequired

The live developer's name when work_state is working_now ("Tomas Ashworth"), else null. The ONLY field a card may use to claim somebody is working, there is no client-side fallback, by design.

was_worked_bystring, nullablerequired

The name of the developer who HAD it, when nobody has it now. Null while someone is on it, and null when nobody ever was, so an abandoned card can say "Hugh Draper stopped" instead of going anonymous.

went_quiet_atstring, nullablerequired

When that developer was last seen, so a card can say "quiet since 09:14".

released_atstring, nullablerequired

When this ticket was last taken off a dead thread and handed back. claimed_at is PRESERVED alongside it, so "started" and "given back" are both readable.

ai_contextobjectoptional

R15, DETAIL ONLY. Present on getBoardTicket, ABSENT on a list row and on a mutation echo: rendering it for 123 rows would be megabytes on every board refresh. A client keeps the copy it loaded when a fresher row arrives without one, rather than blanking the brief mid-session.

3 child fields
markdownstringrequired

The whole brief as one paste-and-go document. This is what an agent is handed.

sectionsarray of objectrequired
4 child fields
keystringrequired

Stable and machine-readable: what_needs_doing, at_a_glance, pr, history, ...

titlestringrequired
bodystring, nullablerequired
itemsarray of stringrequired

List-shaped sections (dependencies, sub-tasks, history) come back as items.

generated_atstringrequired

When the server composed it, so a stale brief is visible as stale.

curl -X POST "https://www.membber.com/api/v1/board/tickets" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Morning class",
    "section": "customer",
    "type": "task",
    "priority": "critical",
    "why": "<why>",
    "what": "<what>"
  }'
Response, 200
{
  "ticket": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "source": "<source>",
    "pr_number": 1,
    "branch": "<branch>",
    "section": "<section>",
    "col": "<col>",
    "founder_col": "<founder_col>",
    "pr_state": "<pr_state>",
    "type": "<type>",
    "prio": "<prio>",
    "title": "Morning class",
    "raw_title": "<raw_title>",
    "summary": "<summary>",
    "after_done": "<after_done>",
    "why": "<why>",
    "what_it_does": "<what_it_does>",
    "user_story": "<user_story>",
    "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
    "when_ts": "<when_ts>",
    "content_source": "<content_source>",
    "synced_at": "<synced_at>",
    "assignee": "<assignee>",
    "claimed_at": "<claimed_at>",
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "depends_on": [
      "<depends_on>"
    ],
    "labels": [
      "<label>"
    ],
    "notes": "Added at the front desk",
    "founder_ord": 1,
    "created_ts": "<created_ts>",
    "updated_ts": "<updated_ts>",
    "initiative": "<initiative>",
    "merchant": "<merchant>",
    "addons": [
      "<addon>"
    ],
    "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
    "human_context": "<human_context>",
    "assignee_kind": "human",
    "reopened_at": "<reopened_at>",
    "reopen_count": 1,
    "capture_text": "<capture_text>",
    "subtask_ids": [
      "e8d74bd5-0000-4000-8000-d0c5000000e8"
    ],
    "children_done": -9007199254740991,
    "created_by_founder": true,
    "founder_owned": true,
    "lifecycle": "awaiting_triage",
    "work_state": "working_now",
    "worked_by": "<worked_by>",
    "was_worked_by": "<was_worked_by>",
    "went_quiet_at": "<went_quiet_at>",
    "released_at": "<released_at>",
    "ai_context": {
      "markdown": "<markdown>",
      "sections": [
        {
          "key": "<key>",
          "title": "Morning class",
          "body": "<body>",
          "items": [
            "<item>"
          ]
        }
      ],
      "generated_at": "<generated_at>"
    }
  }
}

Get one board ticket with BOTH contexts: the short human line and the complete AI bundle.

R13-R15, the heart of the board redesign. Returns the ticket, its comments, its full history and its attachments, plus two deliberately asymmetric views of the same work: `human_context` is one short plain-English line saying what needs doing, and `ai_context` / `ai_context_markdown` is everything, assembled server-side, so an agent needs no other source.

Parameters

idstringpathrequired

Response, 200

ticketobjectrequired

Every column, PLUS the rendered `ai_context` bundle the sheet draws.

50 child fields
idstringrequired
sourcestringrequired
pr_numbernumber, nullablerequired
branchstring, nullablerequired
sectionstring, nullablerequired
colstring, nullablerequired

Resolved column: the founder override (founder_col) if set, else the synced col.

founder_colstring, nullablerequired

The founder column override, or null when the item is not founder-pinned.

pr_statestring, nullablerequired
typestringrequired

Resolved type: type_override if set, else the synced type.

priostringrequired

Resolved priority: prio_override if set, else the synced prio.

titlestring, nullablerequired

Resolved title: display_title ?? title ?? raw_title.

raw_titlestring, nullablerequired
summarystring, nullablerequired

The plain-terms "In short" TL;DR every ticket opens with.

after_donestring, nullablerequired

Founder-action rows: "What happens after you tick it", the post-done "Next:" card.

whystring, nullablerequired
what_it_doesstring, nullablerequired
user_storystring, nullablerequired
epic_idstring, nullablerequired
when_tsstring, nullablerequired

ISO date (YYYY-MM-DD) the work landed / was raised.

content_sourcestring, nullablerequired
synced_atstringrequired

ISO timestamp of the last sync write.

assigneestring, nullablerequired
claimed_atstring, nullablerequired
session_idstring, nullablerequired
depends_onarray of stringrequired

R16: ticket ids this one is waiting on. Empty = not blocked.

labelsarray of stringrequired
notesstring, nullablerequired
founder_ordnumber, nullablerequired
created_tsstring, nullablerequired

The time dimension: when the item was born (PR createdAt / FA added date). Null when the source has no date.

updated_tsstring, nullablerequired

When the item last moved at its source (PR updatedAt). Null when unknown.

initiativestring, nullablerequired

The ONE initiative/vertical this ticket serves: gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform. 'platform' is the honest cross-cutting none. Null = never classified. (Read kept permissive: the vocabulary may grow without a migration.)

merchantstring, nullablerequired

Canonical store slug when the ticket is about ONE real store (busy-beans, tc60, flour-bloom, larkfield, …). Null = no single store / never classified.

addonsarray of stringrequired

Add-on slugs from the real addons catalog (order_collect, bookings, gym_management, own_app, retention_engine) plus family-level 'moments'. Null = never classified; [] = classified as none, the distinction is deliberate and kept on the wire.

parent_idstring, nullablerequired

R17: the ticket this is a sub-task of, or null when it stands alone.

human_contextstring, nullablerequired

R14: the SHORT plain-English line, AS AUTHORED. Null when nobody has written one. getBoardTicket returns a derived fallback in its own top-level human_context field; this column stays honest about whether a human actually wrote it.

assignee_kindenumrequired

'human' with an assignee = a person owns it. 'ai' with a session_id = THAT thread owns it. 'ai' with no session_id = the pool, any thread may pick it up. null = unassigned.

humanai
reopened_atstring, nullablerequired

R9: when it last came back out of Done, or null if it never has.

reopen_countintegerrequired

R9: how many times it has come back out of Done. 0 for most tickets.

capture_textstring, nullablerequired

Founder quick capture (18 Aug 2026): his VERBATIM words at capture time (source 'founder-capture'). Kept forever, even after triage fills the brief, his words are the record. Null on every other source.

subtask_idsarray of stringrequired

R17: ids of the tickets whose parent_id is this one. Computed on read, never written.

children_doneintegerrequired

Honest per-ticket progress (19 Aug 2026): how many of subtask_ids sit in a RESOLVED column of 'done', the DONE half of the card's "3 of 8". The TOTAL is subtask_ids.length, so both halves have one source and cannot disagree. 0 when there are no children. Never a percentage.

created_by_founderbooleanrequired

True when the FOUNDER filed this row himself, across all three intake paths: quick capture (source='founder-capture'), the ⊕ New-ticket sheet (source='manual' AND content_source='founder'), or a bug he raised that was later filed against a board_inbox row with asking_session_id NULL. Computed by the one server-side predicate (isFounderCreated in src/lib/board/resolve.ts), never re-derived from the id prefix, which is wrong on the live board.

founder_ownedbooleanrequired

True when the row is HIS: everything created_by_founder covers, PLUS his own errand list (source='founder-action', the FA items). Strictly wider than created_by_founder and never narrower. Two fields because the questions differ, provenance decides whether a trust-loop push fires (only for what he actually filed), while the board's "Yours" view decides what he sees. Computed by isFounderOwned in src/lib/board/resolve.ts.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

Computed, never written. working_now = a LIVE session (silent < 45 min, the board_liveness_window) holds it by session_id OR assignee=label OR branch=branch, the ONLY state a screen may present as being worked. started_nobody_on_it = in a started column and NO session has ever held it. abandoned_was_held = a session did hold it and that thread is finished or long silent. not_applicable = not in a started column at all (To-do, Done, Blocked, Triage have no liveness question to answer).

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
worked_bystring, nullablerequired

The live developer's name when work_state is working_now ("Tomas Ashworth"), else null. The ONLY field a card may use to claim somebody is working, there is no client-side fallback, by design.

was_worked_bystring, nullablerequired

The name of the developer who HAD it, when nobody has it now. Null while someone is on it, and null when nobody ever was, so an abandoned card can say "Hugh Draper stopped" instead of going anonymous.

went_quiet_atstring, nullablerequired

When that developer was last seen, so a card can say "quiet since 09:14".

released_atstring, nullablerequired

When this ticket was last taken off a dead thread and handed back. claimed_at is PRESERVED alongside it, so "started" and "given back" are both readable.

ai_contextobjectoptional

R15, DETAIL ONLY. Present on getBoardTicket, ABSENT on a list row and on a mutation echo: rendering it for 123 rows would be megabytes on every board refresh. A client keeps the copy it loaded when a fresher row arrives without one, rather than blanking the brief mid-session.

3 child fields
markdownstringrequired

The whole brief as one paste-and-go document. This is what an agent is handed.

sectionsarray of objectrequired
4 child fields
keystringrequired

Stable and machine-readable: what_needs_doing, at_a_glance, pr, history, ...

titlestringrequired
bodystring, nullablerequired
itemsarray of stringrequired

List-shaped sections (dependencies, sub-tasks, history) come back as items.

generated_atstringrequired

When the server composed it, so a stale brief is visible as stale.

commentsarray of objectrequired
6 child fields
idstringrequired
ticket_idstringrequired
authorstringrequired
bodystringrequired
mediaarray of objectrequired

Pre-denormalised media stored on the comment (may be empty).

created_atstringrequired
activityarray of objectrequired
6 child fields
idnumberrequired
ticket_idstringrequired
actorstringrequired
verbstringrequired
detailstring, nullablerequired
created_atstringrequired
attachmentsarray of objectrequired
14 child fields
idstringrequired
ticket_idstring, nullablerequired
inbox_idstring, nullablerequired
kindstringrequired
captionstring, nullablerequired
widthinteger, nullablerequired
heightinteger, nullablerequired
created_atstringrequired
storage_pathstring, nullablerequired

The object in the private bucket. Stable; `url` is derived from it on every read. Null on a pure-link row.

external_urlstring, nullableoptional

The destination of a `kind='link'` row (a Figma page, a Loom). Null on stored rows.

urlstring, nullablerequired

For stored rows: a signed URL valid for one hour (null if signing failed; the row is still real). For a link row: `external_url` passed through untouched, one field to open, whatever the kind.

content_typestring, nullableoptional

MIME type of the stored object (e.g. video/quicktime, application/pdf). Null on legacy image rows.

filenamestring, nullableoptional

Original filename as picked on the device, for display. Null on screenshots.

byte_sizeinteger, nullableoptional

Verified size of the stored object in bytes. Null on legacy rows.

linksobjectrequired

R16/R17: every link resolved to a title and a column, so all four are tappable.

4 child fields
parentobjectrequired
8 child fields
idstringrequired
titlestring, nullablerequired
colstring, nullablerequired
typestringrequired
priostringrequired
reopen_countintegerrequired

So a link can show the reopened badge without a second fetch.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

So a linked sub-task or dependency can show that somebody is on it without a second fetch. Same computed field, same one definition.

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
subtasksarray of objectrequired
8 child fields
idstringrequired
titlestring, nullablerequired
colstring, nullablerequired
typestringrequired
priostringrequired
reopen_countintegerrequired

So a link can show the reopened badge without a second fetch.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

So a linked sub-task or dependency can show that somebody is on it without a second fetch. Same computed field, same one definition.

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
depends_onarray of objectrequired
8 child fields
idstringrequired
titlestring, nullablerequired
colstring, nullablerequired
typestringrequired
priostringrequired
reopen_countintegerrequired

So a link can show the reopened badge without a second fetch.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

So a linked sub-task or dependency can show that somebody is on it without a second fetch. Same computed field, same one definition.

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
blocksarray of objectrequired

What is waiting on THIS ticket.

8 child fields
idstringrequired
titlestring, nullablerequired
colstring, nullablerequired
typestringrequired
priostringrequired
reopen_countintegerrequired

So a link can show the reopened badge without a second fetch.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

So a linked sub-task or dependency can show that somebody is on it without a second fetch. Same computed field, same one definition.

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
sessionobjectrequired

R5: the thread holding it, with live status and WIP. Null when nobody is.

19 child fields
session_idstringrequired
labelstring, nullablerequired

The human-readable name of the thread, or null if it never sent one.

kindstringrequired

'ai' or 'human'.

branchstring, nullablerequired
worktreestring, nullablerequired
started_atstringrequired
last_seen_atstringrequired
finished_atstring, nullablerequired
is_livebooleanrequired

R24: not finished AND seen inside the staleness window. This is what "a live session" means.

ticket_idsarray of stringrequired
in_progressintegerrequired

R25: how many tickets this thread holds in Doing.

in_reviewintegerrequired
testingintegerrequired
todointegerrequired
doneintegerrequired
goalstring, nullablerequired

The mission this thread carries, in the words it was given ("fix click and collect bugs"). From the kit's `hello --goal`, or the spawn request's instructions for a phone-spawned thread. Null when never stated, a client renders nothing, never an invented mission.

filed_countintegerrequired

Tickets this developer FILED, counted across every real linkage (content_source `thread:<label>` + activity `filed` rows), not just live session_id ownership. The zero-tickets fix (17 Aug 2026): 28 of 38 developers read zero under the session_id-only join while 27 of them had real history on other linkages.

finished_countintegerrequired

Linked tickets now sitting in Done, across every linkage.

history_countintegerrequired

Distinct tickets with ANY real linkage to this developer (held / filed / touched / branch). Views excluded.

human_contextstring, nullablerequired

R13/R14: the SHORT plain-English line, for a person. The authored one if there is one, else the best REAL line from the ticket's own fields, trimmed but never rewritten. Null when there is genuinely nothing, so the app shows a designed blank instead of an invented sentence.

human_context_sourceenumrequired

Where human_context came from, so the UI can never pass a PR summary off as the founder's own words. Null exactly when human_context is null.

authoredsummarywhywhat_it_doesuser_story
ai_contextobjectrequired
16 child fields
generated_atstringrequired
ticketobjectrequired
50 child fields
idstringrequired
sourcestringrequired
pr_numbernumber, nullablerequired
branchstring, nullablerequired
sectionstring, nullablerequired
colstring, nullablerequired

Resolved column: the founder override (founder_col) if set, else the synced col.

founder_colstring, nullablerequired

The founder column override, or null when the item is not founder-pinned.

pr_statestring, nullablerequired
typestringrequired

Resolved type: type_override if set, else the synced type.

priostringrequired

Resolved priority: prio_override if set, else the synced prio.

titlestring, nullablerequired

Resolved title: display_title ?? title ?? raw_title.

raw_titlestring, nullablerequired
summarystring, nullablerequired

The plain-terms "In short" TL;DR every ticket opens with.

after_donestring, nullablerequired

Founder-action rows: "What happens after you tick it", the post-done "Next:" card.

whystring, nullablerequired
what_it_doesstring, nullablerequired
user_storystring, nullablerequired
epic_idstring, nullablerequired
when_tsstring, nullablerequired

ISO date (YYYY-MM-DD) the work landed / was raised.

content_sourcestring, nullablerequired
synced_atstringrequired

ISO timestamp of the last sync write.

assigneestring, nullablerequired
claimed_atstring, nullablerequired
session_idstring, nullablerequired
depends_onarray of stringrequired

R16: ticket ids this one is waiting on. Empty = not blocked.

labelsarray of stringrequired
notesstring, nullablerequired
founder_ordnumber, nullablerequired
created_tsstring, nullablerequired

The time dimension: when the item was born (PR createdAt / FA added date). Null when the source has no date.

updated_tsstring, nullablerequired

When the item last moved at its source (PR updatedAt). Null when unknown.

initiativestring, nullablerequired

The ONE initiative/vertical this ticket serves: gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform. 'platform' is the honest cross-cutting none. Null = never classified. (Read kept permissive: the vocabulary may grow without a migration.)

merchantstring, nullablerequired

Canonical store slug when the ticket is about ONE real store (busy-beans, tc60, flour-bloom, larkfield, …). Null = no single store / never classified.

addonsarray of stringrequired

Add-on slugs from the real addons catalog (order_collect, bookings, gym_management, own_app, retention_engine) plus family-level 'moments'. Null = never classified; [] = classified as none, the distinction is deliberate and kept on the wire.

parent_idstring, nullablerequired

R17: the ticket this is a sub-task of, or null when it stands alone.

human_contextstring, nullablerequired

R14: the SHORT plain-English line, AS AUTHORED. Null when nobody has written one. getBoardTicket returns a derived fallback in its own top-level human_context field; this column stays honest about whether a human actually wrote it.

assignee_kindenumrequired

'human' with an assignee = a person owns it. 'ai' with a session_id = THAT thread owns it. 'ai' with no session_id = the pool, any thread may pick it up. null = unassigned.

humanai
reopened_atstring, nullablerequired

R9: when it last came back out of Done, or null if it never has.

reopen_countintegerrequired

R9: how many times it has come back out of Done. 0 for most tickets.

capture_textstring, nullablerequired

Founder quick capture (18 Aug 2026): his VERBATIM words at capture time (source 'founder-capture'). Kept forever, even after triage fills the brief, his words are the record. Null on every other source.

subtask_idsarray of stringrequired

R17: ids of the tickets whose parent_id is this one. Computed on read, never written.

children_doneintegerrequired

Honest per-ticket progress (19 Aug 2026): how many of subtask_ids sit in a RESOLVED column of 'done', the DONE half of the card's "3 of 8". The TOTAL is subtask_ids.length, so both halves have one source and cannot disagree. 0 when there are no children. Never a percentage.

created_by_founderbooleanrequired

True when the FOUNDER filed this row himself, across all three intake paths: quick capture (source='founder-capture'), the ⊕ New-ticket sheet (source='manual' AND content_source='founder'), or a bug he raised that was later filed against a board_inbox row with asking_session_id NULL. Computed by the one server-side predicate (isFounderCreated in src/lib/board/resolve.ts), never re-derived from the id prefix, which is wrong on the live board.

founder_ownedbooleanrequired

True when the row is HIS: everything created_by_founder covers, PLUS his own errand list (source='founder-action', the FA items). Strictly wider than created_by_founder and never narrower. Two fields because the questions differ, provenance decides whether a trust-loop push fires (only for what he actually filed), while the board's "Yours" view decides what he sees. Computed by isFounderOwned in src/lib/board/resolve.ts.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

Computed, never written. working_now = a LIVE session (silent < 45 min, the board_liveness_window) holds it by session_id OR assignee=label OR branch=branch, the ONLY state a screen may present as being worked. started_nobody_on_it = in a started column and NO session has ever held it. abandoned_was_held = a session did hold it and that thread is finished or long silent. not_applicable = not in a started column at all (To-do, Done, Blocked, Triage have no liveness question to answer).

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
worked_bystring, nullablerequired

The live developer's name when work_state is working_now ("Tomas Ashworth"), else null. The ONLY field a card may use to claim somebody is working, there is no client-side fallback, by design.

was_worked_bystring, nullablerequired

The name of the developer who HAD it, when nobody has it now. Null while someone is on it, and null when nobody ever was, so an abandoned card can say "Hugh Draper stopped" instead of going anonymous.

went_quiet_atstring, nullablerequired

When that developer was last seen, so a card can say "quiet since 09:14".

released_atstring, nullablerequired

When this ticket was last taken off a dead thread and handed back. claimed_at is PRESERVED alongside it, so "started" and "given back" are both readable.

ai_contextobjectoptional

R15, DETAIL ONLY. Present on getBoardTicket, ABSENT on a list row and on a mutation echo: rendering it for 123 rows would be megabytes on every board refresh. A client keeps the copy it loaded when a fresher row arrives without one, rather than blanking the brief mid-session.

3 child fields
markdownstringrequired

The whole brief as one paste-and-go document. This is what an agent is handed.

sectionsarray of objectrequired
4 child fields
keystringrequired

Stable and machine-readable: what_needs_doing, at_a_glance, pr, history, ...

titlestringrequired
bodystring, nullablerequired
itemsarray of stringrequired

List-shaped sections (dependencies, sub-tasks, history) come back as items.

generated_atstringrequired

When the server composed it, so a stale brief is visible as stale.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
probjectrequired

Null when no pull request is linked. That is a real fact, not a missing field.

4 child fields
numberintegerrequired
statestring, nullablerequired
branchstring, nullablerequired
urlstringrequired
epicobjectrequired
3 child fields
idstringrequired
titlestring, nullablerequired
colorstring, nullablerequired
parentobjectrequired
8 child fields
idstringrequired
titlestring, nullablerequired
colstring, nullablerequired
typestringrequired
priostringrequired
reopen_countintegerrequired

So a link can show the reopened badge without a second fetch.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

So a linked sub-task or dependency can show that somebody is on it without a second fetch. Same computed field, same one definition.

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
childrenarray of objectrequired

R17: the sub-tasks, resolved to titles and columns.

8 child fields
idstringrequired
titlestring, nullablerequired
colstring, nullablerequired
typestringrequired
priostringrequired
reopen_countintegerrequired

So a link can show the reopened badge without a second fetch.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

So a linked sub-task or dependency can show that somebody is on it without a second fetch. Same computed field, same one definition.

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
dependenciesarray of objectrequired

R16: what this is waiting on, resolved, not bare ids.

8 child fields
idstringrequired
titlestring, nullablerequired
colstring, nullablerequired
typestringrequired
priostringrequired
reopen_countintegerrequired

So a link can show the reopened badge without a second fetch.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

So a linked sub-task or dependency can show that somebody is on it without a second fetch. Same computed field, same one definition.

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
dependentsarray of objectrequired

What is waiting on THIS one, i.e. what it is blocking.

8 child fields
idstringrequired
titlestring, nullablerequired
colstring, nullablerequired
typestringrequired
priostringrequired
reopen_countintegerrequired

So a link can show the reopened badge without a second fetch.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

So a linked sub-task or dependency can show that somebody is on it without a second fetch. Same computed field, same one definition.

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
commentsarray of objectrequired

R18: every comment, oldest first.

6 child fields
idstringrequired
ticket_idstringrequired
authorstringrequired
bodystringrequired
mediaarray of objectrequired

Pre-denormalised media stored on the comment (may be empty).

created_atstringrequired
activityarray of objectrequired

The FULL history of this ticket, oldest first.

6 child fields
idnumberrequired
ticket_idstringrequired
actorstringrequired
verbstringrequired
detailstring, nullablerequired
created_atstringrequired
attachmentsarray of objectrequired
14 child fields
idstringrequired
ticket_idstring, nullablerequired
inbox_idstring, nullablerequired
kindstringrequired
captionstring, nullablerequired
widthinteger, nullablerequired
heightinteger, nullablerequired
created_atstringrequired
storage_pathstring, nullablerequired

The object in the private bucket. Stable; `url` is derived from it on every read. Null on a pure-link row.

external_urlstring, nullableoptional

The destination of a `kind='link'` row (a Figma page, a Loom). Null on stored rows.

urlstring, nullablerequired

For stored rows: a signed URL valid for one hour (null if signing failed; the row is still real). For a link row: `external_url` passed through untouched, one field to open, whatever the kind.

content_typestring, nullableoptional

MIME type of the stored object (e.g. video/quicktime, application/pdf). Null on legacy image rows.

filenamestring, nullableoptional

Original filename as picked on the device, for display. Null on screenshots.

byte_sizeinteger, nullableoptional

Verified size of the stored object in bytes. Null on legacy rows.

sessionobjectrequired

The thread that owns it, with its live status and WIP.

19 child fields
session_idstringrequired
labelstring, nullablerequired

The human-readable name of the thread, or null if it never sent one.

kindstringrequired

'ai' or 'human'.

branchstring, nullablerequired
worktreestring, nullablerequired
started_atstringrequired
last_seen_atstringrequired
finished_atstring, nullablerequired
is_livebooleanrequired

R24: not finished AND seen inside the staleness window. This is what "a live session" means.

ticket_idsarray of stringrequired
in_progressintegerrequired

R25: how many tickets this thread holds in Doing.

in_reviewintegerrequired
testingintegerrequired
todointegerrequired
doneintegerrequired
goalstring, nullablerequired

The mission this thread carries, in the words it was given ("fix click and collect bugs"). From the kit's `hello --goal`, or the spawn request's instructions for a phone-spawned thread. Null when never stated, a client renders nothing, never an invented mission.

filed_countintegerrequired

Tickets this developer FILED, counted across every real linkage (content_source `thread:<label>` + activity `filed` rows), not just live session_id ownership. The zero-tickets fix (17 Aug 2026): 28 of 38 developers read zero under the session_id-only join while 27 of them had real history on other linkages.

finished_countintegerrequired

Linked tickets now sitting in Done, across every linkage.

history_countintegerrequired

Distinct tickets with ANY real linkage to this developer (held / filed / touched / branch). Views excluded.

assignment_historyarray of objectrequired

The ownership-and-movement slice of the history, so who-had-this-when is legible at a glance.

6 child fields
idnumberrequired
ticket_idstringrequired
actorstringrequired
verbstringrequired
detailstring, nullablerequired
created_atstringrequired
labelsarray of stringrequired
notesstring, nullablerequired
ai_context_markdownstringrequired

R15: the same bundle pre-rendered as one markdown string an agent can paste straight into its own context and start work from, with nothing else needed.

viewersarray of objectrequired

Who has OPENED this ticket (not just commented/moved), the surgical activity ask, 15 Aug 2026.

5 child fields
actorstringrequired

Stable machine key: the operator email (human) or the session id (AI). Render names from the roster.

kindstringrequired

'ai' or 'human'.

first_viewed_atstringrequired
last_viewed_atstringrequired
view_countintegerrequired
decisionsarray of objectrequired

Every decision raised AGAINST this ticket (board_inbox rows whose ticket_id is this id), oldest first, the decision thread of the ticket story, for the history timeline. Sourced from board_inbox directly, never from board_activity (whose inbox rows carry ticket_id as `inbox:<uuid>` and would be silently missed).

18 child fields
idstringrequired
titlestringrequired
bodystringrequired
section_hintstring, nullablerequired
priority_hintstring, nullablerequired
reopen_ofstring, nullablerequired

The ticket id this inbox item re-opens, or null for a fresh item.

statusstringrequired

'untriaged', 'triaged' or 'answered'.

created_atstringrequired
triaged_atstring, nullablerequired
attachmentsarray of objectrequired

R19: the screenshots raised WITH it, before it is triaged into a ticket.

14 child fields
idstringrequired
ticket_idstring, nullablerequired
inbox_idstring, nullablerequired
kindstringrequired
captionstring, nullablerequired
widthinteger, nullablerequired
heightinteger, nullablerequired
created_atstringrequired
storage_pathstring, nullablerequired

The object in the private bucket. Stable; `url` is derived from it on every read. Null on a pure-link row.

external_urlstring, nullableoptional

The destination of a `kind='link'` row (a Figma page, a Loom). Null on stored rows.

urlstring, nullablerequired

For stored rows: a signed URL valid for one hour (null if signing failed; the row is still real). For a link row: `external_url` passed through untouched, one field to open, whatever the kind.

content_typestring, nullableoptional

MIME type of the stored object (e.g. video/quicktime, application/pdf). Null on legacy image rows.

filenamestring, nullableoptional

Original filename as picked on the device, for display. Null on screenshots.

byte_sizeinteger, nullableoptional

Verified size of the stored object in bytes. Null on legacy rows.

optionsarray of objectrequired

Decision options, or null for a plain (non-decision) inbox item.

3 child fields
keystringrequired

Stable machine key for this option (never shown to the founder).

labelstringrequired

The plain-English choice the founder reads and taps.

detailstring, nullableoptional

Optional one-line consequence under the label (DECISION-BRIEF step 4).

recommended_optionstring, nullablerequired

The asking thread's recommended option key, or null.

asking_session_idstring, nullablerequired

board_sessions.id of the thread that asked, so the answer can find its way back.

decision_kindenumrequired

How the founder answers this item: single (default) | multi | rank. Meaningless on a plain, option-less inbox item, but always present (defaults to single at the DB layer).

singlemultirank
answerobjectrequired

The founder's answer, or null while the decision is open.

5 child fields
kindenumoptional

The kind this was ANSWERED under, the truth of what was answered even if the item is later re-raised under a different decision_kind. Absent on rows answered before rich decisions shipped.

singlemultirank
option_keystringoptional

single: the chosen option key.

option_keysarray of stringoptional

multi: the picked set (order not meaningful). rank: the FULL order of every option key, best first.

textstringoptional

Free-text answer / rider, when given.

answered_bystringrequired

The verified operator email that answered, never client-supplied.

answered_atstring, nullablerequired
chatarray of objectrequired

The founder asking the raising thread a question WITHOUT resolving the decision (board_inbox_chat). Null = no chat yet. Never touches answer/answered_at/status.

3 child fields
textstringrequired
bystringrequired

Who sent it, the verified operator email, or the asking thread's session id.

atstringrequired

ISO timestamp.

ticket_idstring, nullablerequired

Optional link to the board ticket this decision blocks.

curl -G "https://www.membber.com/api/v1/board/tickets/00000d1b-0000-4000-8000-d0c500000000" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "ticket": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "source": "<source>",
    "pr_number": 1,
    "branch": "<branch>",
    "section": "<section>",
    "col": "<col>",
    "founder_col": "<founder_col>",
    "pr_state": "<pr_state>",
    "type": "<type>",
    "prio": "<prio>",
    "title": "Morning class",
    "raw_title": "<raw_title>",
    "summary": "<summary>",
    "after_done": "<after_done>",
    "why": "<why>",
    "what_it_does": "<what_it_does>",
    "user_story": "<user_story>",
    "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
    "when_ts": "<when_ts>",
    "content_source": "<content_source>",
    "synced_at": "<synced_at>",
    "assignee": "<assignee>",
    "claimed_at": "<claimed_at>",
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "depends_on": [
      "<depends_on>"
    ],
    "labels": [
      "<label>"
    ],
    "notes": "Added at the front desk",
    "founder_ord": 1,
    "created_ts": "<created_ts>",
    "updated_ts": "<updated_ts>",
    "initiative": "<initiative>",
    "merchant": "<merchant>",
    "addons": [
      "<addon>"
    ],
    "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
    "human_context": "<human_context>",
    "assignee_kind": "human",
    "reopened_at": "<reopened_at>",
    "reopen_count": 1,
    "capture_text": "<capture_text>",
    "subtask_ids": [
      "e8d74bd5-0000-4000-8000-d0c5000000e8"
    ],
    "children_done": -9007199254740991,
    "created_by_founder": true,
    "founder_owned": true,
    "lifecycle": "awaiting_triage",
    "work_state": "working_now",
    "worked_by": "<worked_by>",
    "was_worked_by": "<was_worked_by>",
    "went_quiet_at": "<went_quiet_at>",
    "released_at": "<released_at>",
    "ai_context": {
      "markdown": "<markdown>",
      "sections": [
        {
          "key": "<key>",
          "title": "Morning class",
          "body": "<body>",
          "items": [
            "<item>"
          ]
        }
      ],
      "generated_at": "<generated_at>"
    }
  },
  "comments": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
      "author": "<author>",
      "body": "<body>",
      "media": [
        {}
      ],
      "created_at": "<created_at>"
    }
  ],
  "activity": [
    {
      "id": 1,
      "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
      "actor": "<actor>",
      "verb": "<verb>",
      "detail": "<detail>",
      "created_at": "<created_at>"
    }
  ],
  "attachments": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
      "inbox_id": "03d7f974-0000-4000-8000-d0c500000003",
      "kind": "<kind>",
      "caption": "<caption>",
      "width": -9007199254740991,
      "height": -9007199254740991,
      "created_at": "<created_at>",
      "storage_path": "<storage_path>",
      "external_url": "https://example.com/image.jpg",
      "url": "https://example.com/image.jpg",
      "content_type": "<content_type>",
      "filename": "<filename>",
      "byte_size": -9007199254740991
    }
  ],
  "links": {
    "parent": {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "title": "Morning class",
      "col": "<col>",
      "type": "<type>",
      "prio": "<prio>",
      "reopen_count": 1,
      "lifecycle": "awaiting_triage",
      "work_state": "working_now"
    },
    "subtasks": [
      {
        "id": "00000d1b-0000-4000-8000-d0c500000000",
        "title": "Morning class",
        "col": "<col>",
        "type": "<type>",
        "prio": "<prio>",
        "reopen_count": 1,
        "lifecycle": "awaiting_triage",
        "work_state": "working_now"
      }
    ],
    "depends_on": [
      {
        "id": "00000d1b-0000-4000-8000-d0c500000000",
        "title": "Morning class",
        "col": "<col>",
        "type": "<type>",
        "prio": "<prio>",
        "reopen_count": 1,
        "lifecycle": "awaiting_triage",
        "work_state": "working_now"
      }
    ],
    "blocks": [
      {
        "id": "00000d1b-0000-4000-8000-d0c500000000",
        "title": "Morning class",
        "col": "<col>",
        "type": "<type>",
        "prio": "<prio>",
        "reopen_count": 1,
        "lifecycle": "awaiting_triage",
        "work_state": "working_now"
      }
    ]
  },
  "session": {
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "label": "<label>",
    "kind": "<kind>",
    "branch": "<branch>",
    "worktree": "<worktree>",
    "started_at": "<started_at>",
    "last_seen_at": "<last_seen_at>",
    "finished_at": "<finished_at>",
    "is_live": true,
    "ticket_ids": [
      "fbc29b2e-0000-4000-8000-d0c5000000fb"
    ],
    "in_progress": -9007199254740991,
    "in_review": -9007199254740991,
    "testing": -9007199254740991,
    "todo": -9007199254740991,
    "done": -9007199254740991,
    "goal": "<goal>",
    "filed_count": 1,
    "finished_count": 1,
    "history_count": 1
  },
  "human_context": "<human_context>",
  "human_context_source": "authored",
  "ai_context": {
    "generated_at": "<generated_at>",
    "ticket": {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "source": "<source>",
      "pr_number": 1,
      "branch": "<branch>",
      "section": "<section>",
      "col": "<col>",
      "founder_col": "<founder_col>",
      "pr_state": "<pr_state>",
      "type": "<type>",
      "prio": "<prio>",
      "title": "Morning class",
      "raw_title": "<raw_title>",
      "summary": "<summary>",
      "after_done": "<after_done>",
      "why": "<why>",
      "what_it_does": "<what_it_does>",
      "user_story": "<user_story>",
      "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
      "when_ts": "<when_ts>",
      "content_source": "<content_source>",
      "synced_at": "<synced_at>",
      "assignee": "<assignee>",
      "claimed_at": "<claimed_at>",
      "session_id": "630ddf64-0000-4000-8000-d0c500000063",
      "depends_on": [
        "<depends_on>"
      ],
      "labels": [
        "<label>"
      ],
      "notes": "Added at the front desk",
      "founder_ord": 1,
      "created_ts": "<created_ts>",
      "updated_ts": "<updated_ts>",
      "initiative": "<initiative>",
      "merchant": "<merchant>",
      "addons": [
        "<addon>"
      ],
      "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
      "human_context": "<human_context>",
      "assignee_kind": "human",
      "reopened_at": "<reopened_at>",
      "reopen_count": 1,
      "capture_text": "<capture_text>",
      "subtask_ids": [
        "e8d74bd5-0000-4000-8000-d0c5000000e8"
      ],
      "children_done": -9007199254740991,
      "created_by_founder": true,
      "founder_owned": true,
      "lifecycle": "awaiting_triage",
      "work_state": "working_now",
      "worked_by": "<worked_by>",
      "was_worked_by": "<was_worked_by>",
      "went_quiet_at": "<went_quiet_at>",
      "released_at": "<released_at>",
      "ai_context": {
        "markdown": "<markdown>",
        "sections": [
          {
            "key": "<key>",
            "title": "Morning class",
            "body": "<body>",
            "items": [
              null
            ]
          }
        ],
        "generated_at": "<generated_at>"
      }
    },
    "lifecycle": "awaiting_triage",
    "pr": {
      "number": -9007199254740991,
      "state": "<state>",
      "branch": "<branch>",
      "url": "https://example.com/image.jpg"
    },
    "epic": {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "title": "Morning class",
      "color": "<color>"
    },
    "parent": {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "title": "Morning class",
      "col": "<col>",
      "type": "<type>",
      "prio": "<prio>",
      "reopen_count": 1,
      "lifecycle": "awaiting_triage",
      "work_state": "working_now"
    },
    "children": [
      {
        "id": "00000d1b-0000-4000-8000-d0c500000000",
        "title": "Morning class",
        "col": "<col>",
        "type": "<type>",
        "prio": "<prio>",
        "reopen_count": 1,
        "lifecycle": "awaiting_triage",
        "work_state": "working_now"
      }
    ],
    "dependencies": [
      {
        "id": "00000d1b-0000-4000-8000-d0c500000000",
        "title": "Morning class",
        "col": "<col>",
        "type": "<type>",
        "prio": "<prio>",
        "reopen_count": 1,
        "lifecycle": "awaiting_triage",
        "work_state": "working_now"
      }
    ],
    "dependents": [
      {
        "id": "00000d1b-0000-4000-8000-d0c500000000",
        "title": "Morning class",
        "col": "<col>",
        "type": "<type>",
        "prio": "<prio>",
        "reopen_count": 1,
        "lifecycle": "awaiting_triage",
        "work_state": "working_now"
      }
    ],
    "comments": [
      {
        "id": "00000d1b-0000-4000-8000-d0c500000000",
        "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
        "author": "<author>",
        "body": "<body>",
        "media": [
          {}
        ],
        "created_at": "<created_at>"
      }
    ],
    "activity": [
      {
        "id": 1,
        "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
        "actor": "<actor>",
        "verb": "<verb>",
        "detail": "<detail>",
        "created_at": "<created_at>"
      }
    ],
    "attachments": [
      {
        "id": "00000d1b-0000-4000-8000-d0c500000000",
        "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
        "inbox_id": "03d7f974-0000-4000-8000-d0c500000003",
        "kind": "<kind>",
        "caption": "<caption>",
        "width": -9007199254740991,
        "height": -9007199254740991,
        "created_at": "<created_at>",
        "storage_path": "<storage_path>",
        "external_url": "https://example.com/image.jpg",
        "url": "https://example.com/image.jpg",
        "content_type": "<content_type>",
        "filename": "<filename>",
        "byte_size": -9007199254740991
      }
    ],
    "session": {
      "session_id": "630ddf64-0000-4000-8000-d0c500000063",
      "label": "<label>",
      "kind": "<kind>",
      "branch": "<branch>",
      "worktree": "<worktree>",
      "started_at": "<started_at>",
      "last_seen_at": "<last_seen_at>",
      "finished_at": "<finished_at>",
      "is_live": true,
      "ticket_ids": [
        "fbc29b2e-0000-4000-8000-d0c5000000fb"
      ],
      "in_progress": -9007199254740991,
      "in_review": -9007199254740991,
      "testing": -9007199254740991,
      "todo": -9007199254740991,
      "done": -9007199254740991,
      "goal": "<goal>",
      "filed_count": 1,
      "finished_count": 1,
      "history_count": 1
    },
    "assignment_history": [
      {
        "id": 1,
        "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
        "actor": "<actor>",
        "verb": "<verb>",
        "detail": "<detail>",
        "created_at": "<created_at>"
      }
    ],
    "labels": [
      "<label>"
    ],
    "notes": "Added at the front desk"
  },
  "ai_context_markdown": "<ai_context_markdown>",
  "viewers": [
    {
      "actor": "<actor>",
      "kind": "<kind>",
      "first_viewed_at": "<first_viewed_at>",
      "last_viewed_at": "<last_viewed_at>",
      "view_count": 1
    }
  ],
  "decisions": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "title": "Morning class",
      "body": "<body>",
      "section_hint": "<section_hint>",
      "priority_hint": "<priority_hint>",
      "reopen_of": "<reopen_of>",
      "status": "<status>",
      "created_at": "<created_at>",
      "triaged_at": "<triaged_at>",
      "attachments": [
        {
          "id": "00000d1b-0000-4000-8000-d0c500000000",
          "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
          "inbox_id": "03d7f974-0000-4000-8000-d0c500000003",
          "kind": "<kind>",
          "caption": "<caption>",
          "width": -9007199254740991,
          "height": -9007199254740991,
          "created_at": "<created_at>",
          "storage_path": "<storage_path>",
          "external_url": "https://example.com/image.jpg",
          "url": "https://example.com/image.jpg",
          "content_type": "<content_type>",
          "filename": "<filename>",
          "byte_size": -9007199254740991
        }
      ],
      "options": [
        {
          "key": "<key>",
          "label": "<label>",
          "detail": "<detail>"
        }
      ],
      "recommended_option": "<recommended_option>",
      "asking_session_id": "50436e9a-0000-4000-8000-d0c500000050",
      "decision_kind": "single",
      "answer": {
        "kind": "single",
        "option_key": "<option_key>",
        "option_keys": [
          "<option_key>"
        ],
        "text": "<text>",
        "answered_by": "<answered_by>"
      },
      "answered_at": "<answered_at>",
      "chat": [
        {
          "text": "<text>",
          "by": "<by>",
          "at": "<at>"
        }
      ],
      "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb"
    }
  ]
}

Edit a ticket via the founder overlay (title, notes, type, priority, epic, labels).

Parameters

idstringpathrequired

Request body

overlayobjectrequired

Partial patch: a key PRESENT sets it (null clears), a key ABSENT is left untouched. The first six keys are the founder overlay; human_context, parent_id and depends_on are structural facts about the work and land on the ticket itself. summary / why / what_it_does / user_story / section are founder field OVERRIDES stored beside the overlay (sync-proof by construction); initiative / merchant are label corrections the sync already never overwrites.

16 child fields
display_titlestring, nullableoptional

Founder title override; null clears it.

notesstring, nullableoptional
type_overrideenumoptional

R2: the ticket type; null clears the override.

taskbugfeatureepicchore
prio_overridestring, nullableoptional
epic_idstring, nullableoptional
labelsarray of stringoptional

Full label set; null or omitted leaves it untouched.

human_contextstring, nullableoptional

R14: the SHORT plain-English line, in his words. Empty or null clears it and the read falls back to deriving one from the ticket's own fields.

parent_idstring, nullableoptional

R17: make this a sub-task of another ticket; null detaches it. Refused if the parent does not exist, is the ticket itself, or already sits below it (that would be a cycle).

depends_onarray of stringoptional

R16: the FULL set of ticket ids this one waits on, replacing whatever was there. Every id must be a real ticket, so a dependency link always leads somewhere.

summarystring, nullableoptional

Founder 'In short' override (15-140 chars, the kit's own bounds); null clears it back to the synced line.

whystring, nullableoptional

Founder why-this-exists override (≥20 chars, the NO-BLANKS law); null reverts to the synced text.

what_it_doesstring, nullableoptional

Founder what-it-does override (≥20 chars); null reverts to the synced text.

user_storystring, nullableoptional

Founder user-story / done-when override; null reverts to the synced text.

sectionenumoptional

Founder section filing override (same set file-ticket accepts); null reverts to the synced section.

customerbusinesspaymentsverticalsplatformhq
initiativeenumoptional

The ONE initiative this ticket serves ('platform' = honest cross-cutting none).

gymclick-collectbookingsrestaurantsalonbarberloyaltyplatform
merchantstringoptional

Canonical store slug (busy-beans, tc60, flour-bloom, …) when the work is about ONE store.

Response, 200

ticketobjectrequired
50 child fields
idstringrequired
sourcestringrequired
pr_numbernumber, nullablerequired
branchstring, nullablerequired
sectionstring, nullablerequired
colstring, nullablerequired

Resolved column: the founder override (founder_col) if set, else the synced col.

founder_colstring, nullablerequired

The founder column override, or null when the item is not founder-pinned.

pr_statestring, nullablerequired
typestringrequired

Resolved type: type_override if set, else the synced type.

priostringrequired

Resolved priority: prio_override if set, else the synced prio.

titlestring, nullablerequired

Resolved title: display_title ?? title ?? raw_title.

raw_titlestring, nullablerequired
summarystring, nullablerequired

The plain-terms "In short" TL;DR every ticket opens with.

after_donestring, nullablerequired

Founder-action rows: "What happens after you tick it", the post-done "Next:" card.

whystring, nullablerequired
what_it_doesstring, nullablerequired
user_storystring, nullablerequired
epic_idstring, nullablerequired
when_tsstring, nullablerequired

ISO date (YYYY-MM-DD) the work landed / was raised.

content_sourcestring, nullablerequired
synced_atstringrequired

ISO timestamp of the last sync write.

assigneestring, nullablerequired
claimed_atstring, nullablerequired
session_idstring, nullablerequired
depends_onarray of stringrequired

R16: ticket ids this one is waiting on. Empty = not blocked.

labelsarray of stringrequired
notesstring, nullablerequired
founder_ordnumber, nullablerequired
created_tsstring, nullablerequired

The time dimension: when the item was born (PR createdAt / FA added date). Null when the source has no date.

updated_tsstring, nullablerequired

When the item last moved at its source (PR updatedAt). Null when unknown.

initiativestring, nullablerequired

The ONE initiative/vertical this ticket serves: gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform. 'platform' is the honest cross-cutting none. Null = never classified. (Read kept permissive: the vocabulary may grow without a migration.)

merchantstring, nullablerequired

Canonical store slug when the ticket is about ONE real store (busy-beans, tc60, flour-bloom, larkfield, …). Null = no single store / never classified.

addonsarray of stringrequired

Add-on slugs from the real addons catalog (order_collect, bookings, gym_management, own_app, retention_engine) plus family-level 'moments'. Null = never classified; [] = classified as none, the distinction is deliberate and kept on the wire.

parent_idstring, nullablerequired

R17: the ticket this is a sub-task of, or null when it stands alone.

human_contextstring, nullablerequired

R14: the SHORT plain-English line, AS AUTHORED. Null when nobody has written one. getBoardTicket returns a derived fallback in its own top-level human_context field; this column stays honest about whether a human actually wrote it.

assignee_kindenumrequired

'human' with an assignee = a person owns it. 'ai' with a session_id = THAT thread owns it. 'ai' with no session_id = the pool, any thread may pick it up. null = unassigned.

humanai
reopened_atstring, nullablerequired

R9: when it last came back out of Done, or null if it never has.

reopen_countintegerrequired

R9: how many times it has come back out of Done. 0 for most tickets.

capture_textstring, nullablerequired

Founder quick capture (18 Aug 2026): his VERBATIM words at capture time (source 'founder-capture'). Kept forever, even after triage fills the brief, his words are the record. Null on every other source.

subtask_idsarray of stringrequired

R17: ids of the tickets whose parent_id is this one. Computed on read, never written.

children_doneintegerrequired

Honest per-ticket progress (19 Aug 2026): how many of subtask_ids sit in a RESOLVED column of 'done', the DONE half of the card's "3 of 8". The TOTAL is subtask_ids.length, so both halves have one source and cannot disagree. 0 when there are no children. Never a percentage.

created_by_founderbooleanrequired

True when the FOUNDER filed this row himself, across all three intake paths: quick capture (source='founder-capture'), the ⊕ New-ticket sheet (source='manual' AND content_source='founder'), or a bug he raised that was later filed against a board_inbox row with asking_session_id NULL. Computed by the one server-side predicate (isFounderCreated in src/lib/board/resolve.ts), never re-derived from the id prefix, which is wrong on the live board.

founder_ownedbooleanrequired

True when the row is HIS: everything created_by_founder covers, PLUS his own errand list (source='founder-action', the FA items). Strictly wider than created_by_founder and never narrower. Two fields because the questions differ, provenance decides whether a trust-loop push fires (only for what he actually filed), while the board's "Yours" view decides what he sees. Computed by isFounderOwned in src/lib/board/resolve.ts.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

Computed, never written. working_now = a LIVE session (silent < 45 min, the board_liveness_window) holds it by session_id OR assignee=label OR branch=branch, the ONLY state a screen may present as being worked. started_nobody_on_it = in a started column and NO session has ever held it. abandoned_was_held = a session did hold it and that thread is finished or long silent. not_applicable = not in a started column at all (To-do, Done, Blocked, Triage have no liveness question to answer).

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
worked_bystring, nullablerequired

The live developer's name when work_state is working_now ("Tomas Ashworth"), else null. The ONLY field a card may use to claim somebody is working, there is no client-side fallback, by design.

was_worked_bystring, nullablerequired

The name of the developer who HAD it, when nobody has it now. Null while someone is on it, and null when nobody ever was, so an abandoned card can say "Hugh Draper stopped" instead of going anonymous.

went_quiet_atstring, nullablerequired

When that developer was last seen, so a card can say "quiet since 09:14".

released_atstring, nullablerequired

When this ticket was last taken off a dead thread and handed back. claimed_at is PRESERVED alongside it, so "started" and "given back" are both readable.

ai_contextobjectoptional

R15, DETAIL ONLY. Present on getBoardTicket, ABSENT on a list row and on a mutation echo: rendering it for 123 rows would be megabytes on every board refresh. A client keeps the copy it loaded when a fresher row arrives without one, rather than blanking the brief mid-session.

3 child fields
markdownstringrequired

The whole brief as one paste-and-go document. This is what an agent is handed.

sectionsarray of objectrequired
4 child fields
keystringrequired

Stable and machine-readable: what_needs_doing, at_a_glance, pr, history, ...

titlestringrequired
bodystring, nullablerequired
itemsarray of stringrequired

List-shaped sections (dependencies, sub-tasks, history) come back as items.

generated_atstringrequired

When the server composed it, so a stale brief is visible as stale.

curl -X PATCH "https://www.membber.com/api/v1/board/tickets/00000d1b-0000-4000-8000-d0c500000000" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "overlay": {
      "display_title": "<display_title>",
      "notes": "Added at the front desk",
      "type_override": "task",
      "prio_override": "<prio_override>",
      "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
      "labels": [
        "<label>"
      ],
      "human_context": "<human_context>",
      "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
      "depends_on": [
        "<depends_on>"
      ],
      "summary": "<summary>",
      "why": "<why>",
      "what_it_does": "<what_it_does>",
      "user_story": "<user_story>",
      "section": "customer",
      "initiative": "gym",
      "merchant": "<merchant>"
    }
  }'
Response, 200
{
  "ticket": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "source": "<source>",
    "pr_number": 1,
    "branch": "<branch>",
    "section": "<section>",
    "col": "<col>",
    "founder_col": "<founder_col>",
    "pr_state": "<pr_state>",
    "type": "<type>",
    "prio": "<prio>",
    "title": "Morning class",
    "raw_title": "<raw_title>",
    "summary": "<summary>",
    "after_done": "<after_done>",
    "why": "<why>",
    "what_it_does": "<what_it_does>",
    "user_story": "<user_story>",
    "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
    "when_ts": "<when_ts>",
    "content_source": "<content_source>",
    "synced_at": "<synced_at>",
    "assignee": "<assignee>",
    "claimed_at": "<claimed_at>",
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "depends_on": [
      "<depends_on>"
    ],
    "labels": [
      "<label>"
    ],
    "notes": "Added at the front desk",
    "founder_ord": 1,
    "created_ts": "<created_ts>",
    "updated_ts": "<updated_ts>",
    "initiative": "<initiative>",
    "merchant": "<merchant>",
    "addons": [
      "<addon>"
    ],
    "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
    "human_context": "<human_context>",
    "assignee_kind": "human",
    "reopened_at": "<reopened_at>",
    "reopen_count": 1,
    "capture_text": "<capture_text>",
    "subtask_ids": [
      "e8d74bd5-0000-4000-8000-d0c5000000e8"
    ],
    "children_done": -9007199254740991,
    "created_by_founder": true,
    "founder_owned": true,
    "lifecycle": "awaiting_triage",
    "work_state": "working_now",
    "worked_by": "<worked_by>",
    "was_worked_by": "<was_worked_by>",
    "went_quiet_at": "<went_quiet_at>",
    "released_at": "<released_at>",
    "ai_context": {
      "markdown": "<markdown>",
      "sections": [
        {
          "key": "<key>",
          "title": "Morning class",
          "body": "<body>",
          "items": [
            "<item>"
          ]
        }
      ],
      "generated_at": "<generated_at>"
    }
  }
}

Set who owns a ticket: a person, a named AI thread, the AI pool, or nobody.

R4-R7. Replaces claim (R12): a thread is given work, or takes it by moving it to Doing. Assigning to the pool (kind='ai', no session) deliberately leaves `assignee` null, which is what keeps the ticket eligible in the pick-next ranking so any thread can genuinely take it.

Parameters

idstringpathrequired

Request body

kindenumrequired

'human', 'ai', or null to unassign.

humanai
assigneestringoptional

The person's name. REQUIRED when kind is 'human'. Ignored when kind is null.

session_idstring, nullableoptional

The AI thread that owns it. Only meaningful when kind is 'ai': a value names that thread, null or omitted puts it in the pool for any thread. A session that has never sent a heartbeat is refused with 404 rather than written, so a ticket can never be owned by a thread that does not exist.

Response, 200

ticketobjectrequired
50 child fields
idstringrequired
sourcestringrequired
pr_numbernumber, nullablerequired
branchstring, nullablerequired
sectionstring, nullablerequired
colstring, nullablerequired

Resolved column: the founder override (founder_col) if set, else the synced col.

founder_colstring, nullablerequired

The founder column override, or null when the item is not founder-pinned.

pr_statestring, nullablerequired
typestringrequired

Resolved type: type_override if set, else the synced type.

priostringrequired

Resolved priority: prio_override if set, else the synced prio.

titlestring, nullablerequired

Resolved title: display_title ?? title ?? raw_title.

raw_titlestring, nullablerequired
summarystring, nullablerequired

The plain-terms "In short" TL;DR every ticket opens with.

after_donestring, nullablerequired

Founder-action rows: "What happens after you tick it", the post-done "Next:" card.

whystring, nullablerequired
what_it_doesstring, nullablerequired
user_storystring, nullablerequired
epic_idstring, nullablerequired
when_tsstring, nullablerequired

ISO date (YYYY-MM-DD) the work landed / was raised.

content_sourcestring, nullablerequired
synced_atstringrequired

ISO timestamp of the last sync write.

assigneestring, nullablerequired
claimed_atstring, nullablerequired
session_idstring, nullablerequired
depends_onarray of stringrequired

R16: ticket ids this one is waiting on. Empty = not blocked.

labelsarray of stringrequired
notesstring, nullablerequired
founder_ordnumber, nullablerequired
created_tsstring, nullablerequired

The time dimension: when the item was born (PR createdAt / FA added date). Null when the source has no date.

updated_tsstring, nullablerequired

When the item last moved at its source (PR updatedAt). Null when unknown.

initiativestring, nullablerequired

The ONE initiative/vertical this ticket serves: gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform. 'platform' is the honest cross-cutting none. Null = never classified. (Read kept permissive: the vocabulary may grow without a migration.)

merchantstring, nullablerequired

Canonical store slug when the ticket is about ONE real store (busy-beans, tc60, flour-bloom, larkfield, …). Null = no single store / never classified.

addonsarray of stringrequired

Add-on slugs from the real addons catalog (order_collect, bookings, gym_management, own_app, retention_engine) plus family-level 'moments'. Null = never classified; [] = classified as none, the distinction is deliberate and kept on the wire.

parent_idstring, nullablerequired

R17: the ticket this is a sub-task of, or null when it stands alone.

human_contextstring, nullablerequired

R14: the SHORT plain-English line, AS AUTHORED. Null when nobody has written one. getBoardTicket returns a derived fallback in its own top-level human_context field; this column stays honest about whether a human actually wrote it.

assignee_kindenumrequired

'human' with an assignee = a person owns it. 'ai' with a session_id = THAT thread owns it. 'ai' with no session_id = the pool, any thread may pick it up. null = unassigned.

humanai
reopened_atstring, nullablerequired

R9: when it last came back out of Done, or null if it never has.

reopen_countintegerrequired

R9: how many times it has come back out of Done. 0 for most tickets.

capture_textstring, nullablerequired

Founder quick capture (18 Aug 2026): his VERBATIM words at capture time (source 'founder-capture'). Kept forever, even after triage fills the brief, his words are the record. Null on every other source.

subtask_idsarray of stringrequired

R17: ids of the tickets whose parent_id is this one. Computed on read, never written.

children_doneintegerrequired

Honest per-ticket progress (19 Aug 2026): how many of subtask_ids sit in a RESOLVED column of 'done', the DONE half of the card's "3 of 8". The TOTAL is subtask_ids.length, so both halves have one source and cannot disagree. 0 when there are no children. Never a percentage.

created_by_founderbooleanrequired

True when the FOUNDER filed this row himself, across all three intake paths: quick capture (source='founder-capture'), the ⊕ New-ticket sheet (source='manual' AND content_source='founder'), or a bug he raised that was later filed against a board_inbox row with asking_session_id NULL. Computed by the one server-side predicate (isFounderCreated in src/lib/board/resolve.ts), never re-derived from the id prefix, which is wrong on the live board.

founder_ownedbooleanrequired

True when the row is HIS: everything created_by_founder covers, PLUS his own errand list (source='founder-action', the FA items). Strictly wider than created_by_founder and never narrower. Two fields because the questions differ, provenance decides whether a trust-loop push fires (only for what he actually filed), while the board's "Yours" view decides what he sees. Computed by isFounderOwned in src/lib/board/resolve.ts.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

Computed, never written. working_now = a LIVE session (silent < 45 min, the board_liveness_window) holds it by session_id OR assignee=label OR branch=branch, the ONLY state a screen may present as being worked. started_nobody_on_it = in a started column and NO session has ever held it. abandoned_was_held = a session did hold it and that thread is finished or long silent. not_applicable = not in a started column at all (To-do, Done, Blocked, Triage have no liveness question to answer).

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
worked_bystring, nullablerequired

The live developer's name when work_state is working_now ("Tomas Ashworth"), else null. The ONLY field a card may use to claim somebody is working, there is no client-side fallback, by design.

was_worked_bystring, nullablerequired

The name of the developer who HAD it, when nobody has it now. Null while someone is on it, and null when nobody ever was, so an abandoned card can say "Hugh Draper stopped" instead of going anonymous.

went_quiet_atstring, nullablerequired

When that developer was last seen, so a card can say "quiet since 09:14".

released_atstring, nullablerequired

When this ticket was last taken off a dead thread and handed back. claimed_at is PRESERVED alongside it, so "started" and "given back" are both readable.

ai_contextobjectoptional

R15, DETAIL ONLY. Present on getBoardTicket, ABSENT on a list row and on a mutation echo: rendering it for 123 rows would be megabytes on every board refresh. A client keeps the copy it loaded when a fresher row arrives without one, rather than blanking the brief mid-session.

3 child fields
markdownstringrequired

The whole brief as one paste-and-go document. This is what an agent is handed.

sectionsarray of objectrequired
4 child fields
keystringrequired

Stable and machine-readable: what_needs_doing, at_a_glance, pr, history, ...

titlestringrequired
bodystring, nullablerequired
itemsarray of stringrequired

List-shaped sections (dependencies, sub-tasks, history) come back as items.

generated_atstringrequired

When the server composed it, so a stale brief is visible as stale.

curl -X PATCH "https://www.membber.com/api/v1/board/tickets/00000d1b-0000-4000-8000-d0c500000000/assign" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "human"
  }'
Response, 200
{
  "ticket": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "source": "<source>",
    "pr_number": 1,
    "branch": "<branch>",
    "section": "<section>",
    "col": "<col>",
    "founder_col": "<founder_col>",
    "pr_state": "<pr_state>",
    "type": "<type>",
    "prio": "<prio>",
    "title": "Morning class",
    "raw_title": "<raw_title>",
    "summary": "<summary>",
    "after_done": "<after_done>",
    "why": "<why>",
    "what_it_does": "<what_it_does>",
    "user_story": "<user_story>",
    "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
    "when_ts": "<when_ts>",
    "content_source": "<content_source>",
    "synced_at": "<synced_at>",
    "assignee": "<assignee>",
    "claimed_at": "<claimed_at>",
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "depends_on": [
      "<depends_on>"
    ],
    "labels": [
      "<label>"
    ],
    "notes": "Added at the front desk",
    "founder_ord": 1,
    "created_ts": "<created_ts>",
    "updated_ts": "<updated_ts>",
    "initiative": "<initiative>",
    "merchant": "<merchant>",
    "addons": [
      "<addon>"
    ],
    "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
    "human_context": "<human_context>",
    "assignee_kind": "human",
    "reopened_at": "<reopened_at>",
    "reopen_count": 1,
    "capture_text": "<capture_text>",
    "subtask_ids": [
      "e8d74bd5-0000-4000-8000-d0c5000000e8"
    ],
    "children_done": -9007199254740991,
    "created_by_founder": true,
    "founder_owned": true,
    "lifecycle": "awaiting_triage",
    "work_state": "working_now",
    "worked_by": "<worked_by>",
    "was_worked_by": "<was_worked_by>",
    "went_quiet_at": "<went_quiet_at>",
    "released_at": "<released_at>",
    "ai_context": {
      "markdown": "<markdown>",
      "sections": [
        {
          "key": "<key>",
          "title": "Morning class",
          "body": "<body>",
          "items": [
            "<item>"
          ]
        }
      ],
      "generated_at": "<generated_at>"
    }
  }
}

Assign a ticket to a person, or unassign it (legacy shape, kept for the installed app).

Superseded by setBoardTicketOwner (PATCH on the same path), which can also express AI ownership. Kept alive because an installed HQ binary calls it; behaviour is unchanged.

Parameters

idstringpathrequired

Request body

assigneestring, nullablerequired

Who owns it now; null unassigns. Treated as a person.

Response, 200

ticketobjectrequired
50 child fields
idstringrequired
sourcestringrequired
pr_numbernumber, nullablerequired
branchstring, nullablerequired
sectionstring, nullablerequired
colstring, nullablerequired

Resolved column: the founder override (founder_col) if set, else the synced col.

founder_colstring, nullablerequired

The founder column override, or null when the item is not founder-pinned.

pr_statestring, nullablerequired
typestringrequired

Resolved type: type_override if set, else the synced type.

priostringrequired

Resolved priority: prio_override if set, else the synced prio.

titlestring, nullablerequired

Resolved title: display_title ?? title ?? raw_title.

raw_titlestring, nullablerequired
summarystring, nullablerequired

The plain-terms "In short" TL;DR every ticket opens with.

after_donestring, nullablerequired

Founder-action rows: "What happens after you tick it", the post-done "Next:" card.

whystring, nullablerequired
what_it_doesstring, nullablerequired
user_storystring, nullablerequired
epic_idstring, nullablerequired
when_tsstring, nullablerequired

ISO date (YYYY-MM-DD) the work landed / was raised.

content_sourcestring, nullablerequired
synced_atstringrequired

ISO timestamp of the last sync write.

assigneestring, nullablerequired
claimed_atstring, nullablerequired
session_idstring, nullablerequired
depends_onarray of stringrequired

R16: ticket ids this one is waiting on. Empty = not blocked.

labelsarray of stringrequired
notesstring, nullablerequired
founder_ordnumber, nullablerequired
created_tsstring, nullablerequired

The time dimension: when the item was born (PR createdAt / FA added date). Null when the source has no date.

updated_tsstring, nullablerequired

When the item last moved at its source (PR updatedAt). Null when unknown.

initiativestring, nullablerequired

The ONE initiative/vertical this ticket serves: gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform. 'platform' is the honest cross-cutting none. Null = never classified. (Read kept permissive: the vocabulary may grow without a migration.)

merchantstring, nullablerequired

Canonical store slug when the ticket is about ONE real store (busy-beans, tc60, flour-bloom, larkfield, …). Null = no single store / never classified.

addonsarray of stringrequired

Add-on slugs from the real addons catalog (order_collect, bookings, gym_management, own_app, retention_engine) plus family-level 'moments'. Null = never classified; [] = classified as none, the distinction is deliberate and kept on the wire.

parent_idstring, nullablerequired

R17: the ticket this is a sub-task of, or null when it stands alone.

human_contextstring, nullablerequired

R14: the SHORT plain-English line, AS AUTHORED. Null when nobody has written one. getBoardTicket returns a derived fallback in its own top-level human_context field; this column stays honest about whether a human actually wrote it.

assignee_kindenumrequired

'human' with an assignee = a person owns it. 'ai' with a session_id = THAT thread owns it. 'ai' with no session_id = the pool, any thread may pick it up. null = unassigned.

humanai
reopened_atstring, nullablerequired

R9: when it last came back out of Done, or null if it never has.

reopen_countintegerrequired

R9: how many times it has come back out of Done. 0 for most tickets.

capture_textstring, nullablerequired

Founder quick capture (18 Aug 2026): his VERBATIM words at capture time (source 'founder-capture'). Kept forever, even after triage fills the brief, his words are the record. Null on every other source.

subtask_idsarray of stringrequired

R17: ids of the tickets whose parent_id is this one. Computed on read, never written.

children_doneintegerrequired

Honest per-ticket progress (19 Aug 2026): how many of subtask_ids sit in a RESOLVED column of 'done', the DONE half of the card's "3 of 8". The TOTAL is subtask_ids.length, so both halves have one source and cannot disagree. 0 when there are no children. Never a percentage.

created_by_founderbooleanrequired

True when the FOUNDER filed this row himself, across all three intake paths: quick capture (source='founder-capture'), the ⊕ New-ticket sheet (source='manual' AND content_source='founder'), or a bug he raised that was later filed against a board_inbox row with asking_session_id NULL. Computed by the one server-side predicate (isFounderCreated in src/lib/board/resolve.ts), never re-derived from the id prefix, which is wrong on the live board.

founder_ownedbooleanrequired

True when the row is HIS: everything created_by_founder covers, PLUS his own errand list (source='founder-action', the FA items). Strictly wider than created_by_founder and never narrower. Two fields because the questions differ, provenance decides whether a trust-loop push fires (only for what he actually filed), while the board's "Yours" view decides what he sees. Computed by isFounderOwned in src/lib/board/resolve.ts.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

Computed, never written. working_now = a LIVE session (silent < 45 min, the board_liveness_window) holds it by session_id OR assignee=label OR branch=branch, the ONLY state a screen may present as being worked. started_nobody_on_it = in a started column and NO session has ever held it. abandoned_was_held = a session did hold it and that thread is finished or long silent. not_applicable = not in a started column at all (To-do, Done, Blocked, Triage have no liveness question to answer).

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
worked_bystring, nullablerequired

The live developer's name when work_state is working_now ("Tomas Ashworth"), else null. The ONLY field a card may use to claim somebody is working, there is no client-side fallback, by design.

was_worked_bystring, nullablerequired

The name of the developer who HAD it, when nobody has it now. Null while someone is on it, and null when nobody ever was, so an abandoned card can say "Hugh Draper stopped" instead of going anonymous.

went_quiet_atstring, nullablerequired

When that developer was last seen, so a card can say "quiet since 09:14".

released_atstring, nullablerequired

When this ticket was last taken off a dead thread and handed back. claimed_at is PRESERVED alongside it, so "started" and "given back" are both readable.

ai_contextobjectoptional

R15, DETAIL ONLY. Present on getBoardTicket, ABSENT on a list row and on a mutation echo: rendering it for 123 rows would be megabytes on every board refresh. A client keeps the copy it loaded when a fresher row arrives without one, rather than blanking the brief mid-session.

3 child fields
markdownstringrequired

The whole brief as one paste-and-go document. This is what an agent is handed.

sectionsarray of objectrequired
4 child fields
keystringrequired

Stable and machine-readable: what_needs_doing, at_a_glance, pr, history, ...

titlestringrequired
bodystring, nullablerequired
itemsarray of stringrequired

List-shaped sections (dependencies, sub-tasks, history) come back as items.

generated_atstringrequired

When the server composed it, so a stale brief is visible as stale.

curl -X POST "https://www.membber.com/api/v1/board/tickets/00000d1b-0000-4000-8000-d0c500000000/assign" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "assignee": "<assignee>"
  }'
Response, 200
{
  "ticket": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "source": "<source>",
    "pr_number": 1,
    "branch": "<branch>",
    "section": "<section>",
    "col": "<col>",
    "founder_col": "<founder_col>",
    "pr_state": "<pr_state>",
    "type": "<type>",
    "prio": "<prio>",
    "title": "Morning class",
    "raw_title": "<raw_title>",
    "summary": "<summary>",
    "after_done": "<after_done>",
    "why": "<why>",
    "what_it_does": "<what_it_does>",
    "user_story": "<user_story>",
    "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
    "when_ts": "<when_ts>",
    "content_source": "<content_source>",
    "synced_at": "<synced_at>",
    "assignee": "<assignee>",
    "claimed_at": "<claimed_at>",
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "depends_on": [
      "<depends_on>"
    ],
    "labels": [
      "<label>"
    ],
    "notes": "Added at the front desk",
    "founder_ord": 1,
    "created_ts": "<created_ts>",
    "updated_ts": "<updated_ts>",
    "initiative": "<initiative>",
    "merchant": "<merchant>",
    "addons": [
      "<addon>"
    ],
    "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
    "human_context": "<human_context>",
    "assignee_kind": "human",
    "reopened_at": "<reopened_at>",
    "reopen_count": 1,
    "capture_text": "<capture_text>",
    "subtask_ids": [
      "e8d74bd5-0000-4000-8000-d0c5000000e8"
    ],
    "children_done": -9007199254740991,
    "created_by_founder": true,
    "founder_owned": true,
    "lifecycle": "awaiting_triage",
    "work_state": "working_now",
    "worked_by": "<worked_by>",
    "was_worked_by": "<was_worked_by>",
    "went_quiet_at": "<went_quiet_at>",
    "released_at": "<released_at>",
    "ai_context": {
      "markdown": "<markdown>",
      "sections": [
        {
          "key": "<key>",
          "title": "Morning class",
          "body": "<body>",
          "items": [
            "<item>"
          ]
        }
      ],
      "generated_at": "<generated_at>"
    }
  }
}

Every screenshot on a ticket, with fresh signed URLs.

The same list getBoardTicket embeds, on its own so the app can re-sign expired URLs without refetching the whole ticket. Signed URLs last an hour.

auth operator:boardop listBoardTicketAttachmentsrate limitederrors

Parameters

idstringpathrequired

Response, 200

attachmentsarray of objectrequired
14 child fields
idstringrequired
ticket_idstring, nullablerequired
inbox_idstring, nullablerequired
kindstringrequired
captionstring, nullablerequired
widthinteger, nullablerequired
heightinteger, nullablerequired
created_atstringrequired
storage_pathstring, nullablerequired

The object in the private bucket. Stable; `url` is derived from it on every read. Null on a pure-link row.

external_urlstring, nullableoptional

The destination of a `kind='link'` row (a Figma page, a Loom). Null on stored rows.

urlstring, nullablerequired

For stored rows: a signed URL valid for one hour (null if signing failed; the row is still real). For a link row: `external_url` passed through untouched, one field to open, whatever the kind.

content_typestring, nullableoptional

MIME type of the stored object (e.g. video/quicktime, application/pdf). Null on legacy image rows.

filenamestring, nullableoptional

Original filename as picked on the device, for display. Null on screenshots.

byte_sizeinteger, nullableoptional

Verified size of the stored object in bytes. Null on legacy rows.

curl -G "https://www.membber.com/api/v1/board/tickets/00000d1b-0000-4000-8000-d0c500000000/attachments" \
  -H "Authorization: Bearer $MEMBBER_TOKEN"
Response, 200
{
  "attachments": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
      "inbox_id": "03d7f974-0000-4000-8000-d0c500000003",
      "kind": "<kind>",
      "caption": "<caption>",
      "width": -9007199254740991,
      "height": -9007199254740991,
      "created_at": "<created_at>",
      "storage_path": "<storage_path>",
      "external_url": "https://example.com/image.jpg",
      "url": "https://example.com/image.jpg",
      "content_type": "<content_type>",
      "filename": "<filename>",
      "byte_size": -9007199254740991
    }
  ]
}

Attach a screenshot (inline), a pre-uploaded video/file, or a link to a ticket.

R19, stated by the founder with "extreme importance": "I will add bugs into this app when using the app. So I'll be taking screenshots and then transcribing them into it. So that's how we add bugs in." The parent is in the PATH, never the body, so a client cannot violate the one-parent-only constraint by sending both or neither. Stored in the PRIVATE board-media bucket and read back as one-hour signed URLs, because these are screenshots of an unreleased product that routinely show real rows. A `kind: "link"` body attaches a pure URL (Figma, Loom, a doc), nothing is uploaded and the destination comes back as `url`.

Parameters

idstringpathrequired

Response, 200

attachmentobjectrequired
14 child fields
idstringrequired
ticket_idstring, nullablerequired
inbox_idstring, nullablerequired
kindstringrequired
captionstring, nullablerequired
widthinteger, nullablerequired
heightinteger, nullablerequired
created_atstringrequired
storage_pathstring, nullablerequired

The object in the private bucket. Stable; `url` is derived from it on every read. Null on a pure-link row.

external_urlstring, nullableoptional

The destination of a `kind='link'` row (a Figma page, a Loom). Null on stored rows.

urlstring, nullablerequired

For stored rows: a signed URL valid for one hour (null if signing failed; the row is still real). For a link row: `external_url` passed through untouched, one field to open, whatever the kind.

content_typestring, nullableoptional

MIME type of the stored object (e.g. video/quicktime, application/pdf). Null on legacy image rows.

filenamestring, nullableoptional

Original filename as picked on the device, for display. Null on screenshots.

byte_sizeinteger, nullableoptional

Verified size of the stored object in bytes. Null on legacy rows.

curl -X POST "https://www.membber.com/api/v1/board/tickets/00000d1b-0000-4000-8000-d0c500000000/attachments" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '"<value>"'
Response, 200
{
  "attachment": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
    "inbox_id": "03d7f974-0000-4000-8000-d0c500000003",
    "kind": "<kind>",
    "caption": "<caption>",
    "width": -9007199254740991,
    "height": -9007199254740991,
    "created_at": "<created_at>",
    "storage_path": "<storage_path>",
    "external_url": "https://example.com/image.jpg",
    "url": "https://example.com/image.jpg",
    "content_type": "<content_type>",
    "filename": "<filename>",
    "byte_size": -9007199254740991
  }
}

Mint a direct-to-storage upload URL for a video or file going onto a ticket.

Step 1 of the large-media path (17 Aug 2026): videos and files are too big for a JSON body in production, so the bytes go phone → storage directly. PUT the bytes to `signed_url`, then POST …/attachments with `storage_path` to make the object a real, kept attachment. The path is server-minted under the ticket's own prefix, a client can never write outside it.

Parameters

idstringpathrequired

Request body

kindenumrequired

What the bytes are. Drives validation + how the app renders the tile.

imagevideofile
content_typestringrequired

The file's MIME type (e.g. video/quicktime, application/pdf).

byte_sizeintegerrequired

Size of the upload in bytes. Images cap at 15 MB, videos/files at 50 MB (the storage ceiling).

filenamestringoptional

Original filename, for display on the ticket.

Response, 200

pathstringrequired

The object path the upload MUST land at, registration refuses any other.

tokenstringrequired

Signed-upload token (storage `upload/sign`).

signed_urlstringrequired

Absolute URL to PUT the bytes to.

curl -X POST "https://www.membber.com/api/v1/board/tickets/00000d1b-0000-4000-8000-d0c500000000/attachments/upload-url" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "image",
    "content_type": "<content_type>",
    "byte_size": 1
  }'
Response, 200
{
  "path": "<path>",
  "token": "<token>",
  "signed_url": "https://example.com/image.jpg"
}

Add a comment to a ticket.

Parameters

idstringpathrequired

Request body

bodystringrequired

The comment text (non-empty).

mediaarray of objectoptional

Optional pre-uploaded media descriptors to denormalise onto the comment.

client_message_idstringoptional

Client-generated idempotency key (same pattern as the messaging architecture's client_message_id). A retry with the same key on the same ticket is a no-op that returns the ORIGINAL comment, never a duplicate, this route is declared idempotency:{mode:"rpc"}, which board_comment now actually enforces.

Response, 200

commentobjectrequired
6 child fields
idstringrequired
ticket_idstringrequired
authorstringrequired
bodystringrequired
mediaarray of objectrequired

Pre-denormalised media stored on the comment (may be empty).

created_atstringrequired
curl -X POST "https://www.membber.com/api/v1/board/tickets/00000d1b-0000-4000-8000-d0c500000000/comments" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "<body>"
  }'
Response, 200
{
  "comment": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "ticket_id": "fbc29b2e-0000-4000-8000-d0c5000000fb",
    "author": "<author>",
    "body": "<body>",
    "media": [
      {}
    ],
    "created_at": "<created_at>"
  }
}

Move a ticket into any column. Blocking needs a reason and an unblock condition.

R3 + R9. Every column is reachable (blocked, todo, doing, review, testing, done). Moving OUT of Done into any active column stamps `reopened_at` and increments `reopen_count`, because a ticket that came back is not a new ticket. Widened from the original To-do/Blocked-only contract; the two old values still behave exactly as they did, so the installed HQ binary is unaffected. Moving to `blocked` REQUIRES `block_reason` + `block_condition` + `block_ref` (20260819_320): a block takes the ticket out of every developer's queue, so it has to say what it is waiting for and how that clears. A `waiting_on_ticket` block also writes `depends_on`. A block on a decision the founder has ALREADY answered is refused. Blocks reverse themselves: answering the decision, or finishing the depended-on ticket, returns the ticket to To-do automatically with an honest activity row.

Parameters

idstringpathrequired

Request body

colenumrequired

R3: any column on the board. From the card you can move it anywhere.

blockedtododoingreviewtestingdone
ordnumberoptional

Fractional sort index within the target column. Omit to leave the existing position alone.

session_idstringoptional

The thread doing the moving. Moving an UNOWNED ticket to `doing` with this set hands that thread ownership in the same write, which is what replaced claim (R12). It never takes a ticket off someone else. A session that has not sent a heartbeat is refused (404), never written.

block_reasonstringoptional

MANDATORY when `col` is `blocked`. Why this is waiting, in a sentence a person can read. A block removes the ticket from EVERY developer's queue, so it has to say what it is waiting for. A block without one is refused (400 BLOCK_NEEDS_REASON).

block_conditionenumoptional

MANDATORY when `col` is `blocked`, the machine-readable condition that will clear the block. Refused without one (400 BLOCK_NEEDS_CONDITION), because a block nothing can clear never comes back.

waiting_on_founderwaiting_on_ticketwaiting_on_assetwaiting_on_external
block_refstringoptional

MANDATORY when `col` is `blocked`, what the condition points at: a board_inbox id, a ticket id, or a description of the asset/external thing.

Response, 200

ticketobjectrequired
50 child fields
idstringrequired
sourcestringrequired
pr_numbernumber, nullablerequired
branchstring, nullablerequired
sectionstring, nullablerequired
colstring, nullablerequired

Resolved column: the founder override (founder_col) if set, else the synced col.

founder_colstring, nullablerequired

The founder column override, or null when the item is not founder-pinned.

pr_statestring, nullablerequired
typestringrequired

Resolved type: type_override if set, else the synced type.

priostringrequired

Resolved priority: prio_override if set, else the synced prio.

titlestring, nullablerequired

Resolved title: display_title ?? title ?? raw_title.

raw_titlestring, nullablerequired
summarystring, nullablerequired

The plain-terms "In short" TL;DR every ticket opens with.

after_donestring, nullablerequired

Founder-action rows: "What happens after you tick it", the post-done "Next:" card.

whystring, nullablerequired
what_it_doesstring, nullablerequired
user_storystring, nullablerequired
epic_idstring, nullablerequired
when_tsstring, nullablerequired

ISO date (YYYY-MM-DD) the work landed / was raised.

content_sourcestring, nullablerequired
synced_atstringrequired

ISO timestamp of the last sync write.

assigneestring, nullablerequired
claimed_atstring, nullablerequired
session_idstring, nullablerequired
depends_onarray of stringrequired

R16: ticket ids this one is waiting on. Empty = not blocked.

labelsarray of stringrequired
notesstring, nullablerequired
founder_ordnumber, nullablerequired
created_tsstring, nullablerequired

The time dimension: when the item was born (PR createdAt / FA added date). Null when the source has no date.

updated_tsstring, nullablerequired

When the item last moved at its source (PR updatedAt). Null when unknown.

initiativestring, nullablerequired

The ONE initiative/vertical this ticket serves: gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform. 'platform' is the honest cross-cutting none. Null = never classified. (Read kept permissive: the vocabulary may grow without a migration.)

merchantstring, nullablerequired

Canonical store slug when the ticket is about ONE real store (busy-beans, tc60, flour-bloom, larkfield, …). Null = no single store / never classified.

addonsarray of stringrequired

Add-on slugs from the real addons catalog (order_collect, bookings, gym_management, own_app, retention_engine) plus family-level 'moments'. Null = never classified; [] = classified as none, the distinction is deliberate and kept on the wire.

parent_idstring, nullablerequired

R17: the ticket this is a sub-task of, or null when it stands alone.

human_contextstring, nullablerequired

R14: the SHORT plain-English line, AS AUTHORED. Null when nobody has written one. getBoardTicket returns a derived fallback in its own top-level human_context field; this column stays honest about whether a human actually wrote it.

assignee_kindenumrequired

'human' with an assignee = a person owns it. 'ai' with a session_id = THAT thread owns it. 'ai' with no session_id = the pool, any thread may pick it up. null = unassigned.

humanai
reopened_atstring, nullablerequired

R9: when it last came back out of Done, or null if it never has.

reopen_countintegerrequired

R9: how many times it has come back out of Done. 0 for most tickets.

capture_textstring, nullablerequired

Founder quick capture (18 Aug 2026): his VERBATIM words at capture time (source 'founder-capture'). Kept forever, even after triage fills the brief, his words are the record. Null on every other source.

subtask_idsarray of stringrequired

R17: ids of the tickets whose parent_id is this one. Computed on read, never written.

children_doneintegerrequired

Honest per-ticket progress (19 Aug 2026): how many of subtask_ids sit in a RESOLVED column of 'done', the DONE half of the card's "3 of 8". The TOTAL is subtask_ids.length, so both halves have one source and cannot disagree. 0 when there are no children. Never a percentage.

created_by_founderbooleanrequired

True when the FOUNDER filed this row himself, across all three intake paths: quick capture (source='founder-capture'), the ⊕ New-ticket sheet (source='manual' AND content_source='founder'), or a bug he raised that was later filed against a board_inbox row with asking_session_id NULL. Computed by the one server-side predicate (isFounderCreated in src/lib/board/resolve.ts), never re-derived from the id prefix, which is wrong on the live board.

founder_ownedbooleanrequired

True when the row is HIS: everything created_by_founder covers, PLUS his own errand list (source='founder-action', the FA items). Strictly wider than created_by_founder and never narrower. Two fields because the questions differ, provenance decides whether a trust-loop push fires (only for what he actually filed), while the board's "Yours" view decides what he sees. Computed by isFounderOwned in src/lib/board/resolve.ts.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

Computed, never written. working_now = a LIVE session (silent < 45 min, the board_liveness_window) holds it by session_id OR assignee=label OR branch=branch, the ONLY state a screen may present as being worked. started_nobody_on_it = in a started column and NO session has ever held it. abandoned_was_held = a session did hold it and that thread is finished or long silent. not_applicable = not in a started column at all (To-do, Done, Blocked, Triage have no liveness question to answer).

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
worked_bystring, nullablerequired

The live developer's name when work_state is working_now ("Tomas Ashworth"), else null. The ONLY field a card may use to claim somebody is working, there is no client-side fallback, by design.

was_worked_bystring, nullablerequired

The name of the developer who HAD it, when nobody has it now. Null while someone is on it, and null when nobody ever was, so an abandoned card can say "Hugh Draper stopped" instead of going anonymous.

went_quiet_atstring, nullablerequired

When that developer was last seen, so a card can say "quiet since 09:14".

released_atstring, nullablerequired

When this ticket was last taken off a dead thread and handed back. claimed_at is PRESERVED alongside it, so "started" and "given back" are both readable.

ai_contextobjectoptional

R15, DETAIL ONLY. Present on getBoardTicket, ABSENT on a list row and on a mutation echo: rendering it for 123 rows would be megabytes on every board refresh. A client keeps the copy it loaded when a fresher row arrives without one, rather than blanking the brief mid-session.

3 child fields
markdownstringrequired

The whole brief as one paste-and-go document. This is what an agent is handed.

sectionsarray of objectrequired
4 child fields
keystringrequired

Stable and machine-readable: what_needs_doing, at_a_glance, pr, history, ...

titlestringrequired
bodystring, nullablerequired
itemsarray of stringrequired

List-shaped sections (dependencies, sub-tasks, history) come back as items.

generated_atstringrequired

When the server composed it, so a stale brief is visible as stale.

curl -X POST "https://www.membber.com/api/v1/board/tickets/00000d1b-0000-4000-8000-d0c500000000/move" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "col": "blocked"
  }'
Response, 200
{
  "ticket": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "source": "<source>",
    "pr_number": 1,
    "branch": "<branch>",
    "section": "<section>",
    "col": "<col>",
    "founder_col": "<founder_col>",
    "pr_state": "<pr_state>",
    "type": "<type>",
    "prio": "<prio>",
    "title": "Morning class",
    "raw_title": "<raw_title>",
    "summary": "<summary>",
    "after_done": "<after_done>",
    "why": "<why>",
    "what_it_does": "<what_it_does>",
    "user_story": "<user_story>",
    "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
    "when_ts": "<when_ts>",
    "content_source": "<content_source>",
    "synced_at": "<synced_at>",
    "assignee": "<assignee>",
    "claimed_at": "<claimed_at>",
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "depends_on": [
      "<depends_on>"
    ],
    "labels": [
      "<label>"
    ],
    "notes": "Added at the front desk",
    "founder_ord": 1,
    "created_ts": "<created_ts>",
    "updated_ts": "<updated_ts>",
    "initiative": "<initiative>",
    "merchant": "<merchant>",
    "addons": [
      "<addon>"
    ],
    "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
    "human_context": "<human_context>",
    "assignee_kind": "human",
    "reopened_at": "<reopened_at>",
    "reopen_count": 1,
    "capture_text": "<capture_text>",
    "subtask_ids": [
      "e8d74bd5-0000-4000-8000-d0c5000000e8"
    ],
    "children_done": -9007199254740991,
    "created_by_founder": true,
    "founder_owned": true,
    "lifecycle": "awaiting_triage",
    "work_state": "working_now",
    "worked_by": "<worked_by>",
    "was_worked_by": "<was_worked_by>",
    "went_quiet_at": "<went_quiet_at>",
    "released_at": "<released_at>",
    "ai_context": {
      "markdown": "<markdown>",
      "sections": [
        {
          "key": "<key>",
          "title": "Morning class",
          "body": "<body>",
          "items": [
            "<item>"
          ]
        }
      ],
      "generated_at": "<generated_at>"
    }
  }
}

Founder drag-priority: place a ticket between two neighbours (fractional founder_ord).

Writes board_task_state.founder_ord via the atomic board_reorder RPC. Unlike move, this never touches the column, it only ranks the ticket within its group. Worker threads honour founder_ord when picking what to build next (board_next_candidates already folds founder-ranked items to the top).

Parameters

idstringpathrequired

Request body

beforestringoptional

The ticket ABOVE the drop position (higher priority), if any.

afterstringoptional

The ticket BELOW the drop position (lower priority), if any.

before_ordnumberoptional

Materialisation fallback: if `before` has never been ranked, the server gives it this founder_ord first (derived client-side from its current display position) so the drop lands durable and exact even on a never-ranked board. Ignored when `before` already has a rank.

after_ordnumberoptional

Materialisation fallback for `after`, same rules as before_ord.

Response, 200

ticketobjectrequired
50 child fields
idstringrequired
sourcestringrequired
pr_numbernumber, nullablerequired
branchstring, nullablerequired
sectionstring, nullablerequired
colstring, nullablerequired

Resolved column: the founder override (founder_col) if set, else the synced col.

founder_colstring, nullablerequired

The founder column override, or null when the item is not founder-pinned.

pr_statestring, nullablerequired
typestringrequired

Resolved type: type_override if set, else the synced type.

priostringrequired

Resolved priority: prio_override if set, else the synced prio.

titlestring, nullablerequired

Resolved title: display_title ?? title ?? raw_title.

raw_titlestring, nullablerequired
summarystring, nullablerequired

The plain-terms "In short" TL;DR every ticket opens with.

after_donestring, nullablerequired

Founder-action rows: "What happens after you tick it", the post-done "Next:" card.

whystring, nullablerequired
what_it_doesstring, nullablerequired
user_storystring, nullablerequired
epic_idstring, nullablerequired
when_tsstring, nullablerequired

ISO date (YYYY-MM-DD) the work landed / was raised.

content_sourcestring, nullablerequired
synced_atstringrequired

ISO timestamp of the last sync write.

assigneestring, nullablerequired
claimed_atstring, nullablerequired
session_idstring, nullablerequired
depends_onarray of stringrequired

R16: ticket ids this one is waiting on. Empty = not blocked.

labelsarray of stringrequired
notesstring, nullablerequired
founder_ordnumber, nullablerequired
created_tsstring, nullablerequired

The time dimension: when the item was born (PR createdAt / FA added date). Null when the source has no date.

updated_tsstring, nullablerequired

When the item last moved at its source (PR updatedAt). Null when unknown.

initiativestring, nullablerequired

The ONE initiative/vertical this ticket serves: gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform. 'platform' is the honest cross-cutting none. Null = never classified. (Read kept permissive: the vocabulary may grow without a migration.)

merchantstring, nullablerequired

Canonical store slug when the ticket is about ONE real store (busy-beans, tc60, flour-bloom, larkfield, …). Null = no single store / never classified.

addonsarray of stringrequired

Add-on slugs from the real addons catalog (order_collect, bookings, gym_management, own_app, retention_engine) plus family-level 'moments'. Null = never classified; [] = classified as none, the distinction is deliberate and kept on the wire.

parent_idstring, nullablerequired

R17: the ticket this is a sub-task of, or null when it stands alone.

human_contextstring, nullablerequired

R14: the SHORT plain-English line, AS AUTHORED. Null when nobody has written one. getBoardTicket returns a derived fallback in its own top-level human_context field; this column stays honest about whether a human actually wrote it.

assignee_kindenumrequired

'human' with an assignee = a person owns it. 'ai' with a session_id = THAT thread owns it. 'ai' with no session_id = the pool, any thread may pick it up. null = unassigned.

humanai
reopened_atstring, nullablerequired

R9: when it last came back out of Done, or null if it never has.

reopen_countintegerrequired

R9: how many times it has come back out of Done. 0 for most tickets.

capture_textstring, nullablerequired

Founder quick capture (18 Aug 2026): his VERBATIM words at capture time (source 'founder-capture'). Kept forever, even after triage fills the brief, his words are the record. Null on every other source.

subtask_idsarray of stringrequired

R17: ids of the tickets whose parent_id is this one. Computed on read, never written.

children_doneintegerrequired

Honest per-ticket progress (19 Aug 2026): how many of subtask_ids sit in a RESOLVED column of 'done', the DONE half of the card's "3 of 8". The TOTAL is subtask_ids.length, so both halves have one source and cannot disagree. 0 when there are no children. Never a percentage.

created_by_founderbooleanrequired

True when the FOUNDER filed this row himself, across all three intake paths: quick capture (source='founder-capture'), the ⊕ New-ticket sheet (source='manual' AND content_source='founder'), or a bug he raised that was later filed against a board_inbox row with asking_session_id NULL. Computed by the one server-side predicate (isFounderCreated in src/lib/board/resolve.ts), never re-derived from the id prefix, which is wrong on the live board.

founder_ownedbooleanrequired

True when the row is HIS: everything created_by_founder covers, PLUS his own errand list (source='founder-action', the FA items). Strictly wider than created_by_founder and never narrower. Two fields because the questions differ, provenance decides whether a trust-loop push fires (only for what he actually filed), while the board's "Yours" view decides what he sees. Computed by isFounderOwned in src/lib/board/resolve.ts.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

Computed, never written. working_now = a LIVE session (silent < 45 min, the board_liveness_window) holds it by session_id OR assignee=label OR branch=branch, the ONLY state a screen may present as being worked. started_nobody_on_it = in a started column and NO session has ever held it. abandoned_was_held = a session did hold it and that thread is finished or long silent. not_applicable = not in a started column at all (To-do, Done, Blocked, Triage have no liveness question to answer).

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
worked_bystring, nullablerequired

The live developer's name when work_state is working_now ("Tomas Ashworth"), else null. The ONLY field a card may use to claim somebody is working, there is no client-side fallback, by design.

was_worked_bystring, nullablerequired

The name of the developer who HAD it, when nobody has it now. Null while someone is on it, and null when nobody ever was, so an abandoned card can say "Hugh Draper stopped" instead of going anonymous.

went_quiet_atstring, nullablerequired

When that developer was last seen, so a card can say "quiet since 09:14".

released_atstring, nullablerequired

When this ticket was last taken off a dead thread and handed back. claimed_at is PRESERVED alongside it, so "started" and "given back" are both readable.

ai_contextobjectoptional

R15, DETAIL ONLY. Present on getBoardTicket, ABSENT on a list row and on a mutation echo: rendering it for 123 rows would be megabytes on every board refresh. A client keeps the copy it loaded when a fresher row arrives without one, rather than blanking the brief mid-session.

3 child fields
markdownstringrequired

The whole brief as one paste-and-go document. This is what an agent is handed.

sectionsarray of objectrequired
4 child fields
keystringrequired

Stable and machine-readable: what_needs_doing, at_a_glance, pr, history, ...

titlestringrequired
bodystring, nullablerequired
itemsarray of stringrequired

List-shaped sections (dependencies, sub-tasks, history) come back as items.

generated_atstringrequired

When the server composed it, so a stale brief is visible as stale.

curl -X POST "https://www.membber.com/api/v1/board/tickets/00000d1b-0000-4000-8000-d0c500000000/reorder" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "before": "<before>",
    "after": "<after>",
    "before_ord": 1,
    "after_ord": 1
  }'
Response, 200
{
  "ticket": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "source": "<source>",
    "pr_number": 1,
    "branch": "<branch>",
    "section": "<section>",
    "col": "<col>",
    "founder_col": "<founder_col>",
    "pr_state": "<pr_state>",
    "type": "<type>",
    "prio": "<prio>",
    "title": "Morning class",
    "raw_title": "<raw_title>",
    "summary": "<summary>",
    "after_done": "<after_done>",
    "why": "<why>",
    "what_it_does": "<what_it_does>",
    "user_story": "<user_story>",
    "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
    "when_ts": "<when_ts>",
    "content_source": "<content_source>",
    "synced_at": "<synced_at>",
    "assignee": "<assignee>",
    "claimed_at": "<claimed_at>",
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "depends_on": [
      "<depends_on>"
    ],
    "labels": [
      "<label>"
    ],
    "notes": "Added at the front desk",
    "founder_ord": 1,
    "created_ts": "<created_ts>",
    "updated_ts": "<updated_ts>",
    "initiative": "<initiative>",
    "merchant": "<merchant>",
    "addons": [
      "<addon>"
    ],
    "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
    "human_context": "<human_context>",
    "assignee_kind": "human",
    "reopened_at": "<reopened_at>",
    "reopen_count": 1,
    "capture_text": "<capture_text>",
    "subtask_ids": [
      "e8d74bd5-0000-4000-8000-d0c5000000e8"
    ],
    "children_done": -9007199254740991,
    "created_by_founder": true,
    "founder_owned": true,
    "lifecycle": "awaiting_triage",
    "work_state": "working_now",
    "worked_by": "<worked_by>",
    "was_worked_by": "<was_worked_by>",
    "went_quiet_at": "<went_quiet_at>",
    "released_at": "<released_at>",
    "ai_context": {
      "markdown": "<markdown>",
      "sections": [
        {
          "key": "<key>",
          "title": "Morning class",
          "body": "<body>",
          "items": [
            "<item>"
          ]
        }
      ],
      "generated_at": "<generated_at>"
    }
  }
}

Shape a founder capture into a full ticket: fill the brief, promote to To-do (unclaimed).

Refuses any row not in col 'triage' (NOT_A_CAPTURE, the harmless triage race: the second developer just picks other work). Enforces every file-ticket floor server-side, sets col='todo' + the full brief + labels, KEEPS capture_text (his words are the record), and writes a 'triaged' activity row so the history reads captured → triaged → … chronologically. Distinct from triageBoardInbox (inbox HINTS only).

Parameters

idstringpathrequired

Request body

titlestringrequired

The real ticket title the developer writes.

sectionenumrequired

Which area this belongs to (same set file-ticket accepts).

customerbusinesspaymentsverticalsplatformhq
typeenumrequired

R2: task | bug | feature | epic | chore.

taskbugfeatureepicchore
priorityenumrequired

The board priority scale.

criticalhighnormallow
summarystringrequired

The plain-terms "In short" line (15-140 chars, mandatory, founder law, 16 Aug 2026).

whystringrequired

NO-BLANKS law: why this exists, in real words (≥20 chars).

whatstringrequired

NO-BLANKS law: what the change does (≥20 chars). Stored as what_it_does.

initiativestringrequired

Mandatory label: the ONE initiative this serves; 'platform' is the honest cross-cutting choice.

merchantstringoptional

Canonical store slug when it is about ONE store.

addonsarray of stringoptional

Add-on slugs this touches ([] = classified as none).

session_idstringoptional

The thread doing the triage, recorded on the activity row. A session that has never sent a heartbeat is refused (404), never written. Triage does NOT claim the ticket.

Response, 200

ticketobjectrequired
50 child fields
idstringrequired
sourcestringrequired
pr_numbernumber, nullablerequired
branchstring, nullablerequired
sectionstring, nullablerequired
colstring, nullablerequired

Resolved column: the founder override (founder_col) if set, else the synced col.

founder_colstring, nullablerequired

The founder column override, or null when the item is not founder-pinned.

pr_statestring, nullablerequired
typestringrequired

Resolved type: type_override if set, else the synced type.

priostringrequired

Resolved priority: prio_override if set, else the synced prio.

titlestring, nullablerequired

Resolved title: display_title ?? title ?? raw_title.

raw_titlestring, nullablerequired
summarystring, nullablerequired

The plain-terms "In short" TL;DR every ticket opens with.

after_donestring, nullablerequired

Founder-action rows: "What happens after you tick it", the post-done "Next:" card.

whystring, nullablerequired
what_it_doesstring, nullablerequired
user_storystring, nullablerequired
epic_idstring, nullablerequired
when_tsstring, nullablerequired

ISO date (YYYY-MM-DD) the work landed / was raised.

content_sourcestring, nullablerequired
synced_atstringrequired

ISO timestamp of the last sync write.

assigneestring, nullablerequired
claimed_atstring, nullablerequired
session_idstring, nullablerequired
depends_onarray of stringrequired

R16: ticket ids this one is waiting on. Empty = not blocked.

labelsarray of stringrequired
notesstring, nullablerequired
founder_ordnumber, nullablerequired
created_tsstring, nullablerequired

The time dimension: when the item was born (PR createdAt / FA added date). Null when the source has no date.

updated_tsstring, nullablerequired

When the item last moved at its source (PR updatedAt). Null when unknown.

initiativestring, nullablerequired

The ONE initiative/vertical this ticket serves: gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform. 'platform' is the honest cross-cutting none. Null = never classified. (Read kept permissive: the vocabulary may grow without a migration.)

merchantstring, nullablerequired

Canonical store slug when the ticket is about ONE real store (busy-beans, tc60, flour-bloom, larkfield, …). Null = no single store / never classified.

addonsarray of stringrequired

Add-on slugs from the real addons catalog (order_collect, bookings, gym_management, own_app, retention_engine) plus family-level 'moments'. Null = never classified; [] = classified as none, the distinction is deliberate and kept on the wire.

parent_idstring, nullablerequired

R17: the ticket this is a sub-task of, or null when it stands alone.

human_contextstring, nullablerequired

R14: the SHORT plain-English line, AS AUTHORED. Null when nobody has written one. getBoardTicket returns a derived fallback in its own top-level human_context field; this column stays honest about whether a human actually wrote it.

assignee_kindenumrequired

'human' with an assignee = a person owns it. 'ai' with a session_id = THAT thread owns it. 'ai' with no session_id = the pool, any thread may pick it up. null = unassigned.

humanai
reopened_atstring, nullablerequired

R9: when it last came back out of Done, or null if it never has.

reopen_countintegerrequired

R9: how many times it has come back out of Done. 0 for most tickets.

capture_textstring, nullablerequired

Founder quick capture (18 Aug 2026): his VERBATIM words at capture time (source 'founder-capture'). Kept forever, even after triage fills the brief, his words are the record. Null on every other source.

subtask_idsarray of stringrequired

R17: ids of the tickets whose parent_id is this one. Computed on read, never written.

children_doneintegerrequired

Honest per-ticket progress (19 Aug 2026): how many of subtask_ids sit in a RESOLVED column of 'done', the DONE half of the card's "3 of 8". The TOTAL is subtask_ids.length, so both halves have one source and cannot disagree. 0 when there are no children. Never a percentage.

created_by_founderbooleanrequired

True when the FOUNDER filed this row himself, across all three intake paths: quick capture (source='founder-capture'), the ⊕ New-ticket sheet (source='manual' AND content_source='founder'), or a bug he raised that was later filed against a board_inbox row with asking_session_id NULL. Computed by the one server-side predicate (isFounderCreated in src/lib/board/resolve.ts), never re-derived from the id prefix, which is wrong on the live board.

founder_ownedbooleanrequired

True when the row is HIS: everything created_by_founder covers, PLUS his own errand list (source='founder-action', the FA items). Strictly wider than created_by_founder and never narrower. Two fields because the questions differ, provenance decides whether a trust-loop push fires (only for what he actually filed), while the board's "Yours" view decides what he sees. Computed by isFounderOwned in src/lib/board/resolve.ts.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

Computed, never written. working_now = a LIVE session (silent < 45 min, the board_liveness_window) holds it by session_id OR assignee=label OR branch=branch, the ONLY state a screen may present as being worked. started_nobody_on_it = in a started column and NO session has ever held it. abandoned_was_held = a session did hold it and that thread is finished or long silent. not_applicable = not in a started column at all (To-do, Done, Blocked, Triage have no liveness question to answer).

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
worked_bystring, nullablerequired

The live developer's name when work_state is working_now ("Tomas Ashworth"), else null. The ONLY field a card may use to claim somebody is working, there is no client-side fallback, by design.

was_worked_bystring, nullablerequired

The name of the developer who HAD it, when nobody has it now. Null while someone is on it, and null when nobody ever was, so an abandoned card can say "Hugh Draper stopped" instead of going anonymous.

went_quiet_atstring, nullablerequired

When that developer was last seen, so a card can say "quiet since 09:14".

released_atstring, nullablerequired

When this ticket was last taken off a dead thread and handed back. claimed_at is PRESERVED alongside it, so "started" and "given back" are both readable.

ai_contextobjectoptional

R15, DETAIL ONLY. Present on getBoardTicket, ABSENT on a list row and on a mutation echo: rendering it for 123 rows would be megabytes on every board refresh. A client keeps the copy it loaded when a fresher row arrives without one, rather than blanking the brief mid-session.

3 child fields
markdownstringrequired

The whole brief as one paste-and-go document. This is what an agent is handed.

sectionsarray of objectrequired
4 child fields
keystringrequired

Stable and machine-readable: what_needs_doing, at_a_glance, pr, history, ...

titlestringrequired
bodystring, nullablerequired
itemsarray of stringrequired

List-shaped sections (dependencies, sub-tasks, history) come back as items.

generated_atstringrequired

When the server composed it, so a stale brief is visible as stale.

curl -X POST "https://www.membber.com/api/v1/board/tickets/00000d1b-0000-4000-8000-d0c500000000/triage" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Morning class",
    "section": "customer",
    "type": "task",
    "priority": "critical",
    "summary": "<summary>",
    "why": "<why>",
    "what": "<what>",
    "initiative": "<initiative>"
  }'
Response, 200
{
  "ticket": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "source": "<source>",
    "pr_number": 1,
    "branch": "<branch>",
    "section": "<section>",
    "col": "<col>",
    "founder_col": "<founder_col>",
    "pr_state": "<pr_state>",
    "type": "<type>",
    "prio": "<prio>",
    "title": "Morning class",
    "raw_title": "<raw_title>",
    "summary": "<summary>",
    "after_done": "<after_done>",
    "why": "<why>",
    "what_it_does": "<what_it_does>",
    "user_story": "<user_story>",
    "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
    "when_ts": "<when_ts>",
    "content_source": "<content_source>",
    "synced_at": "<synced_at>",
    "assignee": "<assignee>",
    "claimed_at": "<claimed_at>",
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "depends_on": [
      "<depends_on>"
    ],
    "labels": [
      "<label>"
    ],
    "notes": "Added at the front desk",
    "founder_ord": 1,
    "created_ts": "<created_ts>",
    "updated_ts": "<updated_ts>",
    "initiative": "<initiative>",
    "merchant": "<merchant>",
    "addons": [
      "<addon>"
    ],
    "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
    "human_context": "<human_context>",
    "assignee_kind": "human",
    "reopened_at": "<reopened_at>",
    "reopen_count": 1,
    "capture_text": "<capture_text>",
    "subtask_ids": [
      "e8d74bd5-0000-4000-8000-d0c5000000e8"
    ],
    "children_done": -9007199254740991,
    "created_by_founder": true,
    "founder_owned": true,
    "lifecycle": "awaiting_triage",
    "work_state": "working_now",
    "worked_by": "<worked_by>",
    "was_worked_by": "<was_worked_by>",
    "went_quiet_at": "<went_quiet_at>",
    "released_at": "<released_at>",
    "ai_context": {
      "markdown": "<markdown>",
      "sections": [
        {
          "key": "<key>",
          "title": "Morning class",
          "body": "<body>",
          "items": [
            "<item>"
          ]
        }
      ],
      "generated_at": "<generated_at>"
    }
  }
}

Record that the operator OPENED this ticket (board_record_view upsert, one row per viewer, a counter, never history spam).

Fire-and-forget from the ticket sheet. The actor is the VERIFIED operator email (kind human), never client-supplied; AI threads record their own views through the thread kit (scripts/board-thread.mjs seen), which stamps the session id with kind ai.

Parameters

idstringpathrequired

Response, 200

viewersarray of objectrequired

The fresh who-opened-this list, so the sheet can render it at once.

5 child fields
actorstringrequired

Stable machine key: the operator email (human) or the session id (AI). Render names from the roster.

kindstringrequired

'ai' or 'human'.

first_viewed_atstringrequired
last_viewed_atstringrequired
view_countintegerrequired
curl -X POST "https://www.membber.com/api/v1/board/tickets/00000d1b-0000-4000-8000-d0c500000000/viewed" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"
Response, 200
{
  "viewers": [
    {
      "actor": "<actor>",
      "kind": "<kind>",
      "first_viewed_at": "<first_viewed_at>",
      "last_viewed_at": "<last_viewed_at>",
      "view_count": 1
    }
  ]
}

Quick capture from the founder's phone: his words become a triage-column ticket instantly.

Inserts source='founder-capture', col='triage', id `F-<md5(client_request_id) prefix>`, capture_text verbatim, raw_title = the first 140 chars of his words (real data, never an invented title). Chips are optional and never guessed. The row is EXCLUDED from pick/claim until a developer triages it into a full brief (triageBoardTicket). Writes a 'captured' activity row, the first line of the ticket's story. Media rides the existing POST /tickets/{id}/attachments route straight after, on the echoed id.

Request body

capture_textstringrequired

The founder's words, verbatim. The only mandatory field, the capture IS his words.

client_request_idstringrequired

Minted ONCE per capture and reused on every retry, the ticket id derives from it, so a retry returns the original row instead of a duplicate.

sectionenumoptional

Optional chip: which app/area this is about. Unset = the triaging developer decides.

customerbusinesspaymentsverticalsplatformhq
initiativestringoptional

Optional chip: the ONE initiative this serves (gym | click-collect | …). Never guessed.

merchantstringoptional

Optional chip: canonical store slug when it is about ONE store.

addonsarray of stringoptional

Optional chips: add-on slugs this touches.

priorityenumoptional

Optional chip: how urgent it felt in the moment. Defaults to normal.

criticalhighnormallow

Response, 200

ticketobjectrequired
50 child fields
idstringrequired
sourcestringrequired
pr_numbernumber, nullablerequired
branchstring, nullablerequired
sectionstring, nullablerequired
colstring, nullablerequired

Resolved column: the founder override (founder_col) if set, else the synced col.

founder_colstring, nullablerequired

The founder column override, or null when the item is not founder-pinned.

pr_statestring, nullablerequired
typestringrequired

Resolved type: type_override if set, else the synced type.

priostringrequired

Resolved priority: prio_override if set, else the synced prio.

titlestring, nullablerequired

Resolved title: display_title ?? title ?? raw_title.

raw_titlestring, nullablerequired
summarystring, nullablerequired

The plain-terms "In short" TL;DR every ticket opens with.

after_donestring, nullablerequired

Founder-action rows: "What happens after you tick it", the post-done "Next:" card.

whystring, nullablerequired
what_it_doesstring, nullablerequired
user_storystring, nullablerequired
epic_idstring, nullablerequired
when_tsstring, nullablerequired

ISO date (YYYY-MM-DD) the work landed / was raised.

content_sourcestring, nullablerequired
synced_atstringrequired

ISO timestamp of the last sync write.

assigneestring, nullablerequired
claimed_atstring, nullablerequired
session_idstring, nullablerequired
depends_onarray of stringrequired

R16: ticket ids this one is waiting on. Empty = not blocked.

labelsarray of stringrequired
notesstring, nullablerequired
founder_ordnumber, nullablerequired
created_tsstring, nullablerequired

The time dimension: when the item was born (PR createdAt / FA added date). Null when the source has no date.

updated_tsstring, nullablerequired

When the item last moved at its source (PR updatedAt). Null when unknown.

initiativestring, nullablerequired

The ONE initiative/vertical this ticket serves: gym | click-collect | bookings | restaurant | salon | barber | loyalty | platform. 'platform' is the honest cross-cutting none. Null = never classified. (Read kept permissive: the vocabulary may grow without a migration.)

merchantstring, nullablerequired

Canonical store slug when the ticket is about ONE real store (busy-beans, tc60, flour-bloom, larkfield, …). Null = no single store / never classified.

addonsarray of stringrequired

Add-on slugs from the real addons catalog (order_collect, bookings, gym_management, own_app, retention_engine) plus family-level 'moments'. Null = never classified; [] = classified as none, the distinction is deliberate and kept on the wire.

parent_idstring, nullablerequired

R17: the ticket this is a sub-task of, or null when it stands alone.

human_contextstring, nullablerequired

R14: the SHORT plain-English line, AS AUTHORED. Null when nobody has written one. getBoardTicket returns a derived fallback in its own top-level human_context field; this column stays honest about whether a human actually wrote it.

assignee_kindenumrequired

'human' with an assignee = a person owns it. 'ai' with a session_id = THAT thread owns it. 'ai' with no session_id = the pool, any thread may pick it up. null = unassigned.

humanai
reopened_atstring, nullablerequired

R9: when it last came back out of Done, or null if it never has.

reopen_countintegerrequired

R9: how many times it has come back out of Done. 0 for most tickets.

capture_textstring, nullablerequired

Founder quick capture (18 Aug 2026): his VERBATIM words at capture time (source 'founder-capture'). Kept forever, even after triage fills the brief, his words are the record. Null on every other source.

subtask_idsarray of stringrequired

R17: ids of the tickets whose parent_id is this one. Computed on read, never written.

children_doneintegerrequired

Honest per-ticket progress (19 Aug 2026): how many of subtask_ids sit in a RESOLVED column of 'done', the DONE half of the card's "3 of 8". The TOTAL is subtask_ids.length, so both halves have one source and cannot disagree. 0 when there are no children. Never a percentage.

created_by_founderbooleanrequired

True when the FOUNDER filed this row himself, across all three intake paths: quick capture (source='founder-capture'), the ⊕ New-ticket sheet (source='manual' AND content_source='founder'), or a bug he raised that was later filed against a board_inbox row with asking_session_id NULL. Computed by the one server-side predicate (isFounderCreated in src/lib/board/resolve.ts), never re-derived from the id prefix, which is wrong on the live board.

founder_ownedbooleanrequired

True when the row is HIS: everything created_by_founder covers, PLUS his own errand list (source='founder-action', the FA items). Strictly wider than created_by_founder and never narrower. Two fields because the questions differ, provenance decides whether a trust-loop push fires (only for what he actually filed), while the board's "Yours" view decides what he sees. Computed by isFounderOwned in src/lib/board/resolve.ts.

lifecycleenumrequired

Computed, never written. reopened = sat in To-do having already been Done at least once; once a thread picks it back up it reads in_progress and reopen_count carries the history. awaiting_triage = a founder capture (col 'triage') no developer has shaped into a brief yet.

awaiting_triagenot_startedreopenedin_progressin_reviewtestingblockeddone
work_stateenumrequired

Computed, never written. working_now = a LIVE session (silent < 45 min, the board_liveness_window) holds it by session_id OR assignee=label OR branch=branch, the ONLY state a screen may present as being worked. started_nobody_on_it = in a started column and NO session has ever held it. abandoned_was_held = a session did hold it and that thread is finished or long silent. not_applicable = not in a started column at all (To-do, Done, Blocked, Triage have no liveness question to answer).

working_nowstarted_nobody_on_itabandoned_was_heldnot_applicable
worked_bystring, nullablerequired

The live developer's name when work_state is working_now ("Tomas Ashworth"), else null. The ONLY field a card may use to claim somebody is working, there is no client-side fallback, by design.

was_worked_bystring, nullablerequired

The name of the developer who HAD it, when nobody has it now. Null while someone is on it, and null when nobody ever was, so an abandoned card can say "Hugh Draper stopped" instead of going anonymous.

went_quiet_atstring, nullablerequired

When that developer was last seen, so a card can say "quiet since 09:14".

released_atstring, nullablerequired

When this ticket was last taken off a dead thread and handed back. claimed_at is PRESERVED alongside it, so "started" and "given back" are both readable.

ai_contextobjectoptional

R15, DETAIL ONLY. Present on getBoardTicket, ABSENT on a list row and on a mutation echo: rendering it for 123 rows would be megabytes on every board refresh. A client keeps the copy it loaded when a fresher row arrives without one, rather than blanking the brief mid-session.

3 child fields
markdownstringrequired

The whole brief as one paste-and-go document. This is what an agent is handed.

sectionsarray of objectrequired
4 child fields
keystringrequired

Stable and machine-readable: what_needs_doing, at_a_glance, pr, history, ...

titlestringrequired
bodystring, nullablerequired
itemsarray of stringrequired

List-shaped sections (dependencies, sub-tasks, history) come back as items.

generated_atstringrequired

When the server composed it, so a stale brief is visible as stale.

curl -X POST "https://www.membber.com/api/v1/board/tickets/capture" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "capture_text": "<capture_text>",
    "client_request_id": "8e99e5bf-0000-4000-8000-d0c50000008e"
  }'
Response, 200
{
  "ticket": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "source": "<source>",
    "pr_number": 1,
    "branch": "<branch>",
    "section": "<section>",
    "col": "<col>",
    "founder_col": "<founder_col>",
    "pr_state": "<pr_state>",
    "type": "<type>",
    "prio": "<prio>",
    "title": "Morning class",
    "raw_title": "<raw_title>",
    "summary": "<summary>",
    "after_done": "<after_done>",
    "why": "<why>",
    "what_it_does": "<what_it_does>",
    "user_story": "<user_story>",
    "epic_id": "a3ea3c95-0000-4000-8000-d0c5000000a3",
    "when_ts": "<when_ts>",
    "content_source": "<content_source>",
    "synced_at": "<synced_at>",
    "assignee": "<assignee>",
    "claimed_at": "<claimed_at>",
    "session_id": "630ddf64-0000-4000-8000-d0c500000063",
    "depends_on": [
      "<depends_on>"
    ],
    "labels": [
      "<label>"
    ],
    "notes": "Added at the front desk",
    "founder_ord": 1,
    "created_ts": "<created_ts>",
    "updated_ts": "<updated_ts>",
    "initiative": "<initiative>",
    "merchant": "<merchant>",
    "addons": [
      "<addon>"
    ],
    "parent_id": "7b66b0d0-0000-4000-8000-d0c50000007b",
    "human_context": "<human_context>",
    "assignee_kind": "human",
    "reopened_at": "<reopened_at>",
    "reopen_count": 1,
    "capture_text": "<capture_text>",
    "subtask_ids": [
      "e8d74bd5-0000-4000-8000-d0c5000000e8"
    ],
    "children_done": -9007199254740991,
    "created_by_founder": true,
    "founder_owned": true,
    "lifecycle": "awaiting_triage",
    "work_state": "working_now",
    "worked_by": "<worked_by>",
    "was_worked_by": "<was_worked_by>",
    "went_quiet_at": "<went_quiet_at>",
    "released_at": "<released_at>",
    "ai_context": {
      "markdown": "<markdown>",
      "sections": [
        {
          "key": "<key>",
          "title": "Morning class",
          "body": "<body>",
          "items": [
            "<item>"
          ]
        }
      ],
      "generated_at": "<generated_at>"
    }
  }
}
WhatsApp
Book a Call
Start Free