# PointStack Agent API PointStack is real-time planning poker. This is the complete HTTP API for AI agents and scripts to create and manage estimation rooms. Base URL: https://pointstack.dev A typical flow: create a room (optionally with a backlog of stories), share the returned URL with your team, and they vote. The first person to join a tokenless room becomes its owner. ## Further reading - https://pointstack.dev/llms/workflows.txt — step-by-step recipes (the "put up these tickets" flow, editing a backlog, acting for a user, polling). - https://pointstack.dev/llms/errors.txt — every error code, how to recover, and how to guide the user when something fails. - https://pointstack.dev/llms/responding.txt — how to phrase replies to the user (room-link handoff, "join first to claim", token prompts) with copy-paste message templates. ## Authentication Three modes, sent as `Authorization: Bearer `: - No token: only `POST /api/v1/rooms` is allowed. It creates a *headless* room (no owner) that the first voter to join Claims as owner. Tokenless creation is rate-limited and may be temporarily unavailable under load (HTTP 503) — fall back to a user token or retry. - Room token (`psr_…`): returned once when a room is created. Grants full read/write/delete on that one room, indefinitely (until the owner revokes it in room settings). - User token (`psk_…`): a user generates one on their Account page. Acts as that user: list their rooms, create rooms, read/write/delete any room they own, and set a room's team. Works until rotated/revoked. ## Limits - Story title: 150 chars. Story details: 5,000 chars. Link: 500 chars. Text past these is truncated silently, not rejected, so trim it yourself if the tail matters. - Up to 200 stories per room, 50 stories per request. Going over is a 400. - A room holds up to 20 connected participants. - Agents cannot set estimates — estimates come from people voting in the room. ## Errors All errors share this shape; `code` is stable, `hint` tells you how to recover: ```json { "error": { "code": "rate_limited", "message": "…", "hint": "…", "docs": "/llms.txt", "retryAfter": 30 } } ``` Codes: `unauthorized` (401), `forbidden` (403), `not_found` (404), `validation_failed` (400), `rate_limited` (429), `tokenless_unavailable` (503). `retryAfter` (seconds) is present on 429/503. ## Addressing a room Everywhere below, `:id` accepts either: - the room's `id`, returned when you create it, or - the six-character **room code** from its URL: `https://pointstack.dev/room/ABC234` → `ABC234`. Case-insensitive. Use the code when a person hands you a room link, which is the usual way you will be pointed at a room somebody else created. You still need a token for it: the owner generates one in room settings (the gear) under "Agent API token". A code on its own grants nothing. ## Endpoints ### GET /api/v1/rooms — list your rooms Auth: **user token only**. A room token is scoped to its own room, so it gets a 403 here rather than an empty list. Returns the rooms that token's owner owns, newest first, archived ones excluded. ```sh curl https://pointstack.dev/api/v1/rooms -H 'authorization: Bearer psk_…' ``` ```json { "rooms": [ { "id": "…", "code": "ABC234", "url": "https://pointstack.dev/room/ABC234", "name": "Sprint 42", "deck": "fibonacci", "teamId": null, "createdAt": "2026-01-01T12:00:00.000Z" } ], "limit": 50 } ``` `?limit=` takes 1 to 200, default 50; anything out of range or unparseable is clamped, not rejected. There is no live state here (no `phase`, no votes): that lives in each room and would mean waking every one of them to build a list. Read a single room when you need it. Use this to find a room by `name` or `code` when the user has not given you a link. If they have given you a link, use the code in it directly; you do not need this call. ### POST /api/v1/rooms — create a room No token → headless room. `Bearer psk_…` → room owned by you. Body (all optional except story titles): ```json { "name": "Sprint 42", "deck": "fibonacci", "autoReveal": false, "autoFinalise": true, "autoActivateNext": true, "allowGuests": true, "allowSpectators": true, "timerEnabled": true, "timerSeconds": 30, "maxModerators": 1, "maxVoters": 20, "teamId": "team_…", "stories": [ { "title": "Login flow", "details": "markdown…", "nameLink": "https://github.com/org/repo/issues/1" } ] } ``` Decks: `fibonacci`, `modified-fibonacci`, `tshirt`, `powers-of-2`, `yes-no`, `rating-5`, `rating-10`. `name` defaults to "Room ". `teamId` requires a user token and team membership. Omit `deck` and the toggles to inherit the team's defaults (or the product defaults for a team-less room), which is usually what you want when creating into a team. `nameLink` is the URL a story's title points to (if the title itself is a URL, it becomes the link automatically). Every setting is described under Room settings below. Response 201: ```json { "id": "…", "code": "ABC234", "url": "https://pointstack.dev/room/ABC234", "roomToken": "psr_…", "owner": null, "expiresAt": "2026-01-01T12:00:00.000Z", "stories": [ { "id": "…", "title": "Login flow", "nameLink": "https://…" } ] } ``` `roomToken` is shown only once — store it. `owner` is null until the room is claimed. `expiresAt` is when an unclaimed room is swept (null once claimed). Example: ```sh curl -X POST https://pointstack.dev/api/v1/rooms \ -H 'content-type: application/json' \ -d '{"name":"Sprint 42","stories":[{"title":"Login flow","nameLink":"https://github.com/org/repo/issues/1"}]}' ``` ### GET /api/v1/rooms/:id — read a room Auth: room or user token. Returns settings, stories (with `nameLink`, status, finalEstimate), live `phase`/`currentStoryId`, and participants. ```sh # by id, or by the code from the room's URL — both work curl https://pointstack.dev/api/v1/rooms/ABC234 -H 'authorization: Bearer psr_…' ``` ### PATCH /api/v1/rooms/:id — update settings Provided fields only, so a PATCH with one key leaves everything else alone. The fields, their defaults and what each one does are in Room settings below. Applies live to anyone in the room. ```sh curl -X PATCH https://pointstack.dev/api/v1/rooms/ROOM_ID \ -H 'authorization: Bearer psr_…' -H 'content-type: application/json' \ -d '{"name":"Renamed","autoReveal":true}' ``` ### DELETE /api/v1/rooms/:id — delete (archive) the room Disconnects participants and removes the room. ### POST /api/v1/rooms/:id/stories — add stories Body: `{ "stories": [ { "title": "…", "details": "…", "nameLink": "…" } ] }` (or a single `{ "title": … }`). Returns the created stories with their ids. ### PATCH /api/v1/rooms/:id/stories/:storyId — update a story Body: any of `title`, `details`, `nameLink` (send `null` to clear the link). Never affects votes/phase. Omitted fields are left unchanged. ### DELETE /api/v1/rooms/:id/stories/:storyId — delete a story Removes it regardless of state. If it was the story being played, the round ends, and the room then plays the next pending story unless `autoActivateNext` is off. So `currentStoryId` may well be a different story after this call, not null. ## Room settings Every field below is accepted by both `POST /api/v1/rooms` (create) and `PATCH /api/v1/rooms/:id` (update). Omit a field on create to inherit the team's default, or the product default shown here for a team-less room. The wording is the room's own settings screen, so a change you make through the API means exactly what a person sees in the app. Read these rather than inferring from the field name: `autoFinalise` in particular does not mean "finalise after reveal". | Field | Type | Default | What it does | | --- | --- | --- | --- | | `name` | string, 1 to 50 chars | `Room ` | The room's name. | | `deck` | deck id | `fibonacci` | Which cards voters get. See Decks above. | | `autoReveal` | boolean | `false` | Reveal as soon as everyone has voted. | | `autoFinalise` | boolean | `true` | Lock in a revealed story's estimate when you play another one. It fires on leaving a revealed story, not on revealing it: a revealed story you never move off stays revealed, unfinalised, forever. | | `autoActivateNext` | boolean | `true` | Move to the next pending story whenever the room finishes with the one it is playing: finalised, skipped or deleted. Only for the story actually being played, so tidying up something further down the backlog leaves the room where it is. Nobody has to pick the next one each time. | | `allowGuests` | boolean | `true` | Whether anyone with the link can join. Off closes the room to its team: only the owner and members of `teamId` are let in, in any seat, and a closed room with no team is the owner alone. This is about who belongs in the room, not how they signed in, so a guest session on the team is admitted and a Google account off it is not. | | `allowSpectators` | boolean | `true` | Whether the people who can already join may watch instead of estimating. Turning this off asks any spectators to become voters or leave, rather than converting or removing them. It does not decide who gets in; `allowGuests` does. | | `showSpecialCards` | boolean | `true` | Deal the `?` and `☕` cards alongside the numbers. Off leaves both out, for a room that wants an estimate from everyone. | | `timerEnabled` | boolean | `true` | Whether a moderator may put a countdown on a round. The countdown reveals the round when it runs out. | | `timerSeconds` | 10 to 600 | `30` | How long that countdown runs. Meaningless while `timerEnabled` is false. | | `maxModerators` | 1 to 5 | `1` | How many people can run this room. | | `maxVoters` | 2 to 20 | `20` | How many can hold a voting seat. Once taken, new arrivals join as spectators, or are refused the room when spectators are off too. | | `teamId` | string or null | none | Which team owns the room. User token only, and you must be a member. Moving a room into a team adopts that team's defaults immediately, overriding any setting sent in the same request. Detaching (`null`) changes no other setting. | A number outside its range is clamped, not refused: `timerSeconds: 9999` stores 600 and `maxVoters: 1` stores 2. Read the room back with `GET` if the exact value matters to you. Lowering `maxModerators` or `maxVoters` never unseats anyone already in the room. It only stops more people taking that role. Settings apply live: anyone already in the room sees the change without reloading.