REST API: build on top of Looptro
Looptro exposes a typed REST API for retros, posts, votes, action items, and team management. Same surface the web client uses, described by OpenAPI.
Updated
The Looptro API is the same surface the web client uses. Every endpoint is described by OpenAPI; the typed TypeScript client in client/src/lib/api-types.ts is regenerated from the live schema. If you’re building anything against Looptro (a custom integration, a dashboard, a Slack bot beyond the built-in one), this is where to start.
For AI agents specifically, the MCP server is usually the right surface (capability-scoped tokens, structured tool calls, audit trail). The REST API is for everything else.
Base URL
| Environment | Base URL |
|---|---|
| Hosted | https://app.looptro.dev/api |
| Dev (worktree) | http://localhost:$SERVER_PORT/api (default 8589) |
All endpoints below are relative to that base.
Authentication
Two paths in.
HttpOnly cookie sessions. Same flow the web client uses. Sign up or log in, the server sets looptro_access (15-minute JWT) + looptro_refresh (30-day rotation) cookies, both HttpOnly + Secure + SameSite=Strict. Every subsequent request goes through with credentials: 'include' (or -b $JAR in curl). Refresh tokens rotate on every call to POST /api/auth/refresh.
Personal access tokens. Capability-scoped, team-scoped, revocable. Mint via POST /api/teams/{team_id}/agent-tokens (from the cookie session — the mint endpoint is session-only so a leaked PAT can’t bootstrap sibling tokens). The plaintext is returned once in the wire form rtl_pat_<id>_<secret>. Send it as Authorization: Bearer <token>. Same authz checks as cookie sessions; the difference is that every PAT-driven action is audit-logged with actor_type='agent' plus the token id. See the MCP docs for the OAuth-based discovery + device-code flow agents can use to mint tokens without manual setup.
There is no API-key flow for human users beyond the cookie path. If you need scripted access, use a PAT (even if your “agent” is a cron job).
Quickstart
The commands below walk through signup → create team → create retro → add a post. Run them in order; cookies and IDs carry forward.
JAR=$(mktemp)
BASE=https://app.looptro.dev/api
1. Sign up
curl -s -c $JAR -b $JAR -X POST $BASE/auth/signup \
-H 'Content-Type: application/json' \
-d '{
"email": "you@example.com",
"passphrase": "correct-horse-battery-staple",
"display_name": "You"
}'
201 Created with the new user record. Cookies land in $JAR. Passphrases must be ≥12 characters.
2. Create a team
TEAM=$(curl -s -b $JAR -X POST $BASE/teams \
-H 'Content-Type: application/json' \
-d '{"name":"Engineering","slug":"engineering"}' \
| jq -r .id)
201 Created. The creator becomes the team owner.
3. Create a retro
RETRO=$(curl -s -b $JAR -X POST $BASE/teams/$TEAM/retros \
-H 'Content-Type: application/json' \
-d '{"title":"Sprint 42","template":"start-stop-continue"}' \
| jq -r .id)
201 Created. Pass exactly one of template (built-in preset: start-stop-continue, mad-sad-glad, 4ls, plus-delta, sailboat, daki), team_template_id (UUID of a team-curated template), or columns (inline column list for ad-hoc retros). The retro starts in active state and reflect phase; pass "state":"scheduled" to set it up day-before.
4. Add a post
COLUMN=$(curl -s -b $JAR $BASE/retros/$RETRO | jq -r '.columns[0].id')
curl -s -b $JAR -X POST $BASE/retros/$RETRO/posts \
-H 'Content-Type: application/json' \
-d "{\"column_id\":\"$COLUMN\",\"body\":\"Pair-program on the auth refactor\"}"
201 Created. The retro broadcasts the new post over its WebSocket to anyone with the board open.
Live updates
GET /api/retros/{id}/ws upgrades to a WebSocket. Same authz (cookie or PAT with retros:read) as the REST endpoints. The server pushes JSON events as state changes — each event has a type discriminator (snake_case) plus the relevant payload. Sample events:
type | Carries |
|---|---|
post_created, post_edited, post_moved | full PostDetail |
post_deleted | post_id |
post_grouped, post_group_renamed | post_id / group_id / title |
vote_toggled | post_id, new authoritative count |
action_item_created / _edited / _deleted | full ActionItemDetail or action_item_id |
retro_settings_changed, retro_title_changed, retro_timer_changed, retro_columns_changed | the changed fields |
presence_joined, presence_left, presence_snapshot, participant_evicted | viewer presence |
ready_changed, ready_reset | the per-actor “I’m done” toggle |
Treat events as cache-invalidation prompts and refetch the affected resource; the REST endpoint stays the source of truth. The full event union is in the OpenAPI doc under RetroEvent.
Errors
All errors are JSON with a detail field.
{ "detail": "team not found" }
| Status | When |
|---|---|
| 400 | Malformed request body or missing required field |
| 401 | No valid session cookie or token |
| 403 | Authenticated but not authorized for this resource (also: PAT missing required scope) |
| 404 | Resource doesn’t exist or isn’t visible to this caller |
| 409 | Conflict (e.g. duplicate slug, accepting an already-consumed invite, deleting a template referenced by an active cycle) |
| 422 | Validation error (e.g. passphrase too short) |
| 429 | Rate limit exceeded; retry after the Retry-After header |
500-class errors collapse to {"detail": "internal error"} with the underlying detail in the server’s structured log. If you see one, please email security@looptro.dev with the timestamp and request id.
Rate limits
Per-IP throttles on the endpoints most worth abusing.
| Endpoint | Sustained | Burst |
|---|---|---|
POST /api/auth/signup | 1 every 12 minutes | 5 |
POST /api/auth/login | 1 every 6 seconds | 10 |
POST /api/invites/accept | 1 every 3 minutes | 20 |
POST /api/retros/{id}/participants | 1 every 3 minutes | 20 |
POST /api/teams/{id}/agent-tokens | 1 every 12 minutes | 5 |
OAuth start / callback | 1 every 6s / 12s | 10 / 5 |
| Everything else (global) | 10 per second | 100 |
Behind shared egress (corporate NAT, office VPN), the same office hits one bucket. If a 10-person engineering team needs to log in simultaneously after a deploy, the burst absorbs it. If you’re integrating from a server-side process, set a sane backoff on 429 with Retry-After.
OpenAPI
GET /openapi.json on any Looptro instance returns the full OpenAPI 3 document. Use it with openapi-typescript or any code generator to mint a typed client in your language of choice. The TypeScript client in this repo is generated this way; see client/src/lib/api-types.ts.
What’s not in the public API yet
- Webhook delivery for retro events. Tracked in the Realtime Collaboration project.
- Pagination cursors as opaque tokens (the v1 surface mostly returns small bounded lists).
- Bulk endpoints for action item sync. The MCP server already exposes single-item primitives; bulk is added when a real workflow needs it.