--- title: "Scrum4Me REST API" status: active audience: [ai-agent, contributor] language: en last_updated: 2026-05-03 --- # Scrum4Me REST API REST-API contract voor Claude Code en andere clients. ## Authenticatie Alle endpoints behalve `GET /api/health` vereisen een Bearer-token: ``` Authorization: Bearer ``` Tokens beheer je via Instellingen → Tokens (`/settings/tokens`). Een token is gekoppeld aan één gebruiker; een demo-account-token kan lezen maar niet schrijven (`403`). ## Status-enums De API gebruikt **lowercase** statussen. De database gebruikt UPPER_SNAKE; de vertaling gebeurt op de boundary. | Entiteit | Waarden | |---|---| | Task status | `todo`, `in_progress`, `review`, `done` | | Story status | `open`, `in_sprint`, `done` | ## Entity codes PBI's, stories en tasks hebben elk een verplichte `code` (max 30 chars, regex `^[A-Za-z0-9._-]+$`) die als stabiele identifier dient binnen het product: - **Auto-generatie** wanneer niet meegegeven: `PBI-N`, `ST-N` (3-digit padded), `T-N` — eigen sequence per product. - **Uniek per `(product_id, code)`** voor alle drie entiteiten. - **Stabiel bij re-parenting**: een task die naar een andere story wordt verplaatst behoudt zijn `code` (Jira-stijl). - POST-body `code` is **optioneel** (server vult bij ontbreken); response bevat `code` altijd. ## Foutcodes | Code | Betekenis | |---|---| | `200` | OK | | `201` | Created | | `400` | Malformed body (bv. ongeldige JSON) | | `401` | Token ontbreekt of ongeldig | | `403` | Token heeft geen toegang (demo-account, geen lid van product) | | `404` | Resource niet gevonden | | `422` | Validatiefout — body is wel-gevormd maar niet acceptabel | | `500` | Onverwachte serverfout | --- ## Endpoints ### `GET /api/health` Health-probe. Geen authenticatie vereist. **Query params:** `?db=1` voegt een DB-ping toe. **Response (200):** ```json { "status": "ok", "version": "0.3.x", "time": "2026-04-26T20:00:00Z" } ``` Met `?db=1`: ```json { "status": "ok", "version": "0.3.x", "time": "...", "database": "ok" } ``` `database` is `"ok"` of `"down"`. De endpoint zelf retourneert altijd `200`. ```bash curl https://scrum4me.app/api/health?db=1 ``` --- ### `GET /api/products` Lijst van actieve producten waar de tokengebruiker eigenaar of lid van is. **Response (200):** ```json [ { "id": "cmofu...", "code": "SCRUM4ME", "name": "Scrum4Me", "description": "...", "repo_url": "https://github.com/...", "definition_of_done": "..." } ] ``` ```bash curl -H "Authorization: Bearer $TOKEN" https://scrum4me.app/api/products ``` --- ### `GET /api/products/:id/claude-context` Bundled context voor Claude Code: product, actieve sprint, volgende story (met tasks) en open todos van de tokengebruiker — in één call. **Response (200):** ```json { "product": { "id", "code", "name", "description", "repo_url", "definition_of_done" }, "active_sprint": { "id": "...", "sprint_goal": "...", "status": "ACTIVE" } | null, "next_story": { "id", "code", "title", "description", "acceptance_criteria", "priority", "status", "tasks": [ { "id", "code", "title", "description", "implementation_plan", "priority", "sort_order", "status" } ] } | null, "open_todos": [ { "id", "title", "description", "created_at" } ] } ``` `open_todos` is gelimiteerd op 50 items, gesorteerd op `created_at` asc. Demo-tokens kunnen dit endpoint lezen. ```bash curl -H "Authorization: Bearer $TOKEN" \ https://scrum4me.app/api/products/$PRODUCT_ID/claude-context ``` --- ### `GET /api/products/:id/next-story` Hoogst geprioriteerde open story in de actieve sprint. **Response (200):** ```json { "id": "...", "code": "ST-356", "title": "Solo Kanban-bord met DnD en Zustand", "description": "...", "acceptance_criteria": "...", "status": "in_sprint", "tasks": [ { "id": "...", "code": "T-42", "title": "Store stores/solo-store.ts", "description": "...", "implementation_plan": null, "priority": 2, "sort_order": 1, "status": "todo" } ] } ``` **Foutcodes:** `404` als geen actieve sprint of geen open stories. --- ### `GET /api/sprints/:id/tasks` Lijst taken van de sprint, geordend op `(story.sort_order, task.priority, task.sort_order)`. **Query params:** `?limit=N` (default 10, max 50) **Response (200):** ```json [ { "id": "...", "code": "ST-356.1", "title": "...", "description": "...", "implementation_plan": null, "story_id": "...", "story_code": "ST-356", "priority": 2, "sort_order": 1, "status": "todo" } ] ``` --- ### `PATCH /api/stories/:id/tasks/reorder` Volgorde van taken binnen een story aanpassen. **Body:** ```json { "task_ids": ["task-id-a", "task-id-b", "task-id-c"] } ``` Alle IDs moeten bij de story horen. **Foutcodes:** `422` bij Zod-fouten of als een task_id niet tot de story behoort. --- ### `PATCH /api/tasks/:id` Status of implementation_plan bijwerken. Minstens één van beide is verplicht. Toegestane status-waarden zijn `todo`, `in_progress` en `done`. `review` wordt door deze endpoint geweigerd zolang de sprint-UI die state niet rendert — gebruik de Kanban-board voor REVIEW-overgangen. **Body:** ```json { "status": "in_progress", "implementation_plan": "..." } ``` **Response (200):** ```json { "id": "...", "status": "in_progress", "implementation_plan": "..." } ``` **Foutcodes:** `422` bij ongeldige body of onbekende status. `403` bij demo-token. --- ### `POST /api/stories/:id/log` Activiteit vastleggen op een story. **Body — IMPLEMENTATION_PLAN:** ```json { "type": "IMPLEMENTATION_PLAN", "content": "Plan: ...", "metadata": { "branch": "feat/x" } } ``` **Body — TEST_RESULT:** ```json { "type": "TEST_RESULT", "content": "Alle tests groen", "status": "PASSED", "metadata": { "ci_run": "..." } } ``` **Body — COMMIT:** ```json { "type": "COMMIT", "content": "Werk afgerond", "commit_hash": "abc123", "commit_message": "feat(ST-XXX): ...", "metadata": { "branch": "feat/x" } } ``` `metadata` is optioneel, vrij JSON-object. **Response (201):** ```json { "id": "...", "created_at": "..." } ``` --- ### `POST /api/todos` Nieuwe todo voor de tokengebruiker. **Body:** ```json { "title": "Een ding doen", "description": "Optionele uitleg, max 2000 tekens", "product_id": "cmof..." } ``` **Response (201):** ```json { "id": "...", "title": "...", "description": "...", "created_at": "..." } ``` --- ### `GET /api/realtime/solo?product_id=...` Server-Sent Events stream voor het Solo Paneel. Wordt gebruikt door de browser-UI (`useSoloRealtime`); voor Claude Code zelden relevant, maar gedocumenteerd voor volledigheid. **Auth:** iron-session cookie of Bearer-token. Demo-tokens mogen lezen. **Query params:** `product_id` (verplicht). **Response:** `text/event-stream`. Stream blijft open tot de client sluit of de server na 240s een hard-close doet (client herconnect dan transparant). **Events:** - `event: ready` — eenmalig direct na connect, met `{ product_id, sprint_id }` als payload. - `event: error` — bij interne fouten (pg connect mislukt e.d.). - `data: {...}` — task/story mutaties die binnen scope vallen (zie hieronder). Payload-shape: ```json { "op": "I" | "U" | "D", "entity": "task" | "story", "id": "cmof...", "story_id": "cmof...", "product_id": "cmof...", "sprint_id": "cmog..." , "assignee_id": "cmof..." , "task_status": "TO_DO" | "IN_PROGRESS" | "REVIEW" | "DONE", "task_title": "...", "task_sort_order": 1, "changed_fields": ["status", "updated_at"] } ``` Niet alle velden zijn altijd aanwezig — `task_*` alleen voor `entity: "task"`, idem `story_*`. `task_status` gebruikt de **DB-enum** (UPPER_SNAKE), niet de lowercase API-vorm. - `: heartbeat` — SSE-comment elke 25s, om proxies keep-alive te houden. Kan genegeerd worden. **Server-side filter:** - `product_id` matcht de query-param - `sprint_id` matcht de actieve sprint van het product - `assignee_id` is gelijk aan de ingelogde user (of `null` voor unassigned-story claims) Niet-matchende events worden gedropt — clients ontvangen geen irrelevante data. **Voorbeeld (browser):** ```js const source = new EventSource('/api/realtime/solo?product_id=cmof...') source.onmessage = (e) => console.log(JSON.parse(e.data)) ``` --- ## Auth — QR-pairing (M10) Drie anonieme/cookie-geauthenticeerde endpoints voor de password-loze inlog via QR-pairing. Worden door de browser gebruikt (niet door Claude Code) — gedocumenteerd voor volledigheid en voor handmatige curl-tests. **Cookie-mechaniek:** `pair/start` zet een korte `s4m_pair`-HttpOnly-cookie (`Path=/api/auth/pair`, `Max-Age=300`, `SameSite=Lax`, `Secure` in productie). `pair/stream` en `pair/claim` authenticeren tegen die cookie. Geheim materiaal zit nooit in URL-paden of querystrings — `mobileSecret` reist alleen via QR- fragment (`#s=…`) en POST-body, `desktopToken` alleen via cookie. ### `POST /api/auth/pair/start` Anon. Maakt een nieuwe `LoginPairing` aan en zet de pre-auth cookie. **Auth:** geen. **Body:** geen. **Rate-limit:** 10 per IP per minuut (zelfde patroon als `/login`). **Response 200:** ```json { "pairingId": "cmoh...", "mobileSecret": "<43-char base64url>", "expiresAt": "2026-04-27T20:30:00.000Z", "qrUrl": "https://.../m/pair#id=cmoh...&s=" } ``` Plus `Set-Cookie: s4m_pair=; HttpOnly; Path=/api/auth/pair; Max-Age=300; SameSite=Lax`. **Foutcodes:** `429` bij rate-limit overschreden. **Voorbeeld:** ```bash curl -i -X POST -c /tmp/jar http://localhost:3000/api/auth/pair/start ``` --- ### `GET /api/auth/pair/stream/:pairingId` Server-Sent Events stream die de desktop opent direct na `pair/start` om op de approve-bevestiging van de mobiel te wachten. **Auth:** `s4m_pair`-cookie. Werkt vanuit `EventSource` met `withCredentials: true`. **Path:** `pairingId` is niet vertrouwelijk; cookie is het bewijs. **Stream-duur:** maximaal 240s (Vercel-buffer onder de 300s `maxDuration`); sluit zodra status `consumed` of `cancelled` doorkomt. **Events:** - `event: state` — eenmalig direct na connect, met `{ pairing_id, status }` (status van pairing op moment van connecten — voorkomt race wanneer approve net vóór SSE-open landt). - `data: {...}` — bij elke status-overgang. Payload: ```json { "op": "I" | "U", "pairing_id": "cmoh...", "status": "pending" | "approved" | "consumed" | "cancelled" } ``` - `: heartbeat` — SSE-comment elke 25s. **Foutcodes:** `401` zonder/foute cookie, `404` als pairing onbekend, `410` als pairing verlopen. **Voorbeeld:** ```bash curl -N -i -b /tmp/jar http://localhost:3000/api/auth/pair/stream/ ``` --- ### `POST /api/auth/pair/claim` Cookie-auth. Atomisch consume van een approved pairing → schrijft de echte `session` cookie zodat de desktop is ingelogd. **Auth:** `s4m_pair`-cookie. **Body:** `{ "pairingId": "cmoh..." }`. **Response 200:** `{ "ok": true }` plus - `Set-Cookie: session=...; HttpOnly; SameSite=Lax` — paired-sessie met `paired: true` en `pairedExpiresAt = now + 8h` payload-velden. - `Set-Cookie: s4m_pair=...; Max-Age=0` — pre-auth cookie wordt gewist. **Foutcodes:** - `400` bij ontbrekende of malformed body - `401` zonder cookie of bij hash-mismatch (cookie matcht geen pairing) - `410` als pairing al consumed/cancelled is (replay) of verlopen **Voorbeeld:** ```bash curl -i -X POST -b /tmp/jar -c /tmp/jar \ -H "Content-Type: application/json" \ -d '{"pairingId":""}' \ http://localhost:3000/api/auth/pair/claim ``` --- ## Notifications — Vraag-antwoord-kanaal (M11) Endpoints voor de Claude vraag-antwoord-flow. De **MCP-tools** in de mcp-repo (`ask_user_question`, `get_question_answer`, `list_open_questions`, `cancel_question`) zijn de primaire schrijf-interface; de endpoints hieronder zijn voor de browser-UI en cron. ### `GET /api/realtime/notifications` Server-Sent Events stream voor de notifications-bell in de NavBar. **User-scoped** — geen `product_id`-param; filtert server-side op alle producten waar de gebruiker eigenaar of teamlid is. **Auth:** iron-session cookie. Demo-gebruikers mogen lezen. **Response:** `text/event-stream`. Stream blijft open tot client sluit of server na 240s een hard-close doet (client herconnect). **Events:** - `event: state` — eenmalig direct na connect, met `{ questions: [...] }` als payload (zelfde shape als de live updates). - `data: {...}` — bij elke status-overgang in `claude_questions`. Payload-shape: ```json { "op": "I" | "U", "entity": "question", "id": "cmoh...", "product_id": "cmoh...", "story_id": "cmoh...", "task_id": "cmoh..." | null, "assignee_id": "cmoh..." | null, "status": "open" | "answered" | "cancelled" | "expired" } ``` Het is een delta — voor de volledige vraag-tekst en options reconnect de client (initial-state-event levert ze opnieuw). - `: heartbeat` — SSE-comment elke 25s. **Server-side filter:** - `payload.entity === 'question'` (`task` en `story` events horen op `/api/realtime/solo`) - `payload.product_id` zit in de set producten met user-access (productAccessFilter) **Voorbeeld:** ```js const source = new EventSource('/api/realtime/notifications', { withCredentials: true }) ``` --- ## Cron — Expire questions ### `POST /api/cron/expire-questions` Vercel cron handler die dagelijks draait. Markeert verlopen open vragen als `expired` en verlopen pending login_pairings als `cancelled`. **Auth:** `Authorization: Bearer ${CRON_SECRET}` — header die Vercel automatisch injecteert wanneer de env-var op de project-omgeving staat. Zonder secret of bij mismatch: 401. **Schedule:** `0 4 * * *` (dagelijks om 04:00 UTC; Vercel Hobby-plan staat alleen daily crons toe — Pro ondersteunt fijnmazigere schedules). **Response 200:** ```json { "expired_questions": 0, "expired_pairings": 0, "ran_at": "2026-04-28T00:00:00.000Z" } ``` **Voorbeeld (handmatige trigger):** ```bash curl -X POST -H "Authorization: Bearer $CRON_SECRET" \ https://your-app.vercel.app/api/cron/expire-questions ``` --- ## Cron — Cleanup agent artifacts ### `POST /api/cron/cleanup-agent-artifacts` Vercel cron handler die dagelijks draait. Verwijdert `FAILED` en `CANCELLED` claude_jobs waarvan `finished_at` ouder is dan 7 dagen. Hard-delete — geen historische waarde; audit-trail zit in git-commits. **Auth:** `Authorization: Bearer ${CRON_SECRET}` — zelfde mechanisme als `/api/cron/expire-questions`. Zonder secret of bij mismatch: 401. **Schedule:** `0 3 * * *` (dagelijks om 03:00 UTC). **Response 200:** ```json { "deleted": 3, "ran_at": "2026-05-01T03:00:00.000Z" } ``` **Voorbeeld (handmatige trigger):** ```bash curl -X POST -H "Authorization: Bearer $CRON_SECRET" \ https://your-app.vercel.app/api/cron/cleanup-agent-artifacts ``` --- ## Voorbeeldworkflow voor Claude Code 1. **Probe:** `GET /api/health?db=1` — bevestig dat de service en DB bereikbaar zijn. 2. **Context:** `GET /api/products/$ID/claude-context` — haal product, sprint, volgende story en todos op in één call. 3. **Plan vastleggen:** `POST /api/stories/$STORY_ID/log` met `type: IMPLEMENTATION_PLAN`. 4. **Per task:** `PATCH /api/tasks/$TASK_ID` met `status: "in_progress"`, daarna met `status: "done"` plus eventueel `implementation_plan`. 5. **Test:** `POST /api/stories/$STORY_ID/log` met `type: TEST_RESULT` en `status: PASSED|FAILED`. 6. **Commit:** `POST /api/stories/$STORY_ID/log` met `type: COMMIT`, `commit_hash`, `commit_message`, optioneel `metadata: { branch }`.