API reference · v1

Pokor Public API

Versioned, JSON-only contract under v1. Authenticate with an API key minted from the API dashboard.

On this page

Base URL

Terminal

All public API endpoints are versioned under /v1/. The API is served from the dedicated API subdomain — application UI and the public API never share the same host.

Authentication & headers

Send your API key as a Bearer token in the Authorization header. Mint and revoke keys from the API dashboard. Keys begin with the prefix pk_ and are shown in plaintext exactly once at creation time.

Required headers

  • Authorization: Bearer pk_… — required on every request.
  • Content-Type: application/json — required when sending a JSON body (POST, PATCH).
  • Accept: application/json — recommended; the API only emits JSON.
Terminal

Email-authenticated users must verify their email before minting or using API keys. Guests cannot use the public API because they cannot create API keys.

Optional key restrictions

API keys can be restricted to up to 50 IP addresses or CIDR ranges and up to 50 origin domains. Origin entries may use a wildcard prefix such as *.example.com. Requests outside a configured allowlist are rejected with ip_not_allowed or origin_not_allowed.

Discovery endpoint

GET /v1/ returns the API name, version, and a link back to this page — useful as a connectivity / authentication smoke test.

JSON

Plans & limits

The Pokor public API mirrors the same plan-based limits enforced in the app. UI-created and API-created resources count together against the same totals. Anything marked with * elsewhere in this reference is plan-gated and points back to this section.

CapabilityFreeOrganizer
Active API keys1Unlimited
Throttle (per key, per minute)30 req/min300 req/min
Synchronous sessions
Asynchronous sessions
Add / update / delete stories on existing sessions
Approval-gated joins (require_approval)Stored as false
API key IP / origin allowlists
Active webhook endpoints1Unlimited

Plan-gated error responses

Plan-gated calls return 403 with a stable code so clients can branch deterministically:

  • api_key_quota_exceeded — beyond your active key quota.
  • async_sessions_paid_only — Free user requesting async mode.
  • story_management_paid_only — Free user mutating an existing session's stories.
JSON
JSON
JSON
Need higher limits or unlock paid features? Upgrade to the Organizer plan from pricing.

JSON, status codes & errors

The API speaks JSON only. Request bodies must be valid JSON and use UTF-8. Timestamps are returned as ISO-8601 strings (for example 2026-04-28T12:34:56+00:00). Identifiers are stable across requests: every resource (sessions, stories, participants, bans, webhooks, deliveries) is addressed by a 26-character uppercase ULID matching [0-9A-HJ-KM-NP-TV-Z]{26} (Crockford Base32, e.g. 01HX7Z3Q2K8B0M4N6P9R1S3T5V). Path parameters such as {session_id}, {story_id}, {participant_id}, and {ban_id} must match this pattern; non-conforming values return 404.

Successful responses

Successful responses are wrapped in a data envelope. Single resources return an object; collections return an array. Empty 204 responses (used by DELETE) have no body.

JSON

Error responses

Errors return an error object with a stable machine-readable code and a human-readable message. Some errors include extra fields documented alongside the code (for example fields on validation errors, limit on plan-limit errors, retry_after_seconds on rate-limit errors).

JSON

Status codes

The API uses standard HTTP status codes to indicate the result of every request.

StatusMeaning
200 OKRequest succeeded; response body contains the resource.
201 CreatedResource was created; response body contains the new resource.
204 No ContentRequest succeeded with no body (used by DELETE).
401 UnauthorizedMissing, invalid, revoked, or expired API key.
403 ForbiddenPlan does not allow this action, or email is unverified.
404 Not Found Resource does not exist or you do not own it. Also returned for unsupported API versions and unknown /v1/ paths.
422 Unprocessable Entity Request body failed validation. The fields object lists invalid fields.
429 Too Many Requests Rate limit exceeded. Retry after the time given in Retry-After.
500 Internal Server Error Unexpected server error. Retry idempotent requests after a short delay.

Common error examples

Authentication (401)

JSON

Validation (422)

JSON

Not found (404)

JSON

Reference: error codes

CodeHTTPMeaning
missing_api_key401No Authorization header.
invalid_api_key401Header present but token does not match a known key.
revoked_api_key401Key was revoked by its owner.
expired_api_key401Key passed its expires_at.
email_not_verified403Owner's email is not verified.
ip_not_allowed403Request IP is outside the API key allowlist.
origin_not_allowed403Request origin is outside the API key allowlist.
api_key_quota_exceeded403Plan-based API key quota reached. *
async_sessions_paid_only403Async sessions require a paid plan. *
story_management_paid_only403 Adding, updating, or deleting stories on existing sessions requires a paid plan. *
join_request_not_found404Join request is missing or expired.
validation_failed422Request body failed validation; see fields.
voted_story_locked422Cannot edit scope on a voted story; re-vote first.
cannot_remove_organizer422The session organizer cannot be removed as a participant.
not_found404Resource missing or not owned by the caller.
unsupported_api_version404Version path is not currently supported.
rate_limit_exceeded429 Too many requests; retry after retry_after_seconds.

Rate limits *

Public API requests are rate limited per API key. The per-minute limit depends on the owning user's plan — see the Plans & limits section for the exact numbers.

Every response includes X-RateLimit-Limit and X-RateLimit-Remaining headers so clients can pace themselves. When the limit is exceeded, the API returns 429 with a Retry-After header (seconds), an X-RateLimit-Reset header (unix timestamp), and a retry_after_seconds field in the body:

HTTP

Sessions

Planning sessions are the top-level resource. The authenticated user is the organizer of every session created through the API. Sessions you do not organize return 404.

List sessions

GET /v1/sessions — return every planning session you organize, ordered by most recently created. Stories on each session are returned in sort_order.

Terminal

Example response (200 OK)

JSON

Get a session

GET /v1/sessions/{session_id} — fetch a single session you organize, including its stories.

Terminal

Example response (200 OK)

JSON

Create a session *

POST /v1/sessions — start a new planning session. Free users can create synchronous sessions only; paid users can create both synchronous and asynchronous sessions.

Request body

  • name (string, optional) — session name; auto-generated when omitted.
  • card_deck (string[], optional) — voting cards; defaults to a Fibonacci deck. ? and are always included.
  • mode (string, optional) — sync (default) or async. async requires a paid plan.*
  • default_vote_window_hours (integer, required for async) — vote window in hours, 1–720.
  • auto_reveal_when_all_voted (boolean, optional) — defaults to true.
  • organizer_votes (boolean, optional) — defaults to true.
  • require_approval (boolean, optional) — paid plans only.*
  • stories (array, optional) — initial backlog. Each story takes title (required), description, link, source_type, source_external_id, source_metadata, vote_opens_at, vote_closes_at.

Example request

Terminal

Example response (201 Created)

JSON

Update a session *

PATCH /v1/sessions/{session_id} partially updates a session you own. Free users can update regular session settings, but mode: "async" and require_approval: true are paid-plan capabilities. For Free users, require_approval: true is stored as false.

Terminal

Example response (200 OK)

JSON

Delete a session

DELETE /v1/sessions/{session_id} deletes a session you organize and notifies connected participants. Returns 204 No Content.

Terminal

Stories *

Stories are scoped under a session. Pass the parent session id in the URL. Voted stories are scope-locked: editing title, description, link, or vote windows on a voted story returns 422 with code voted_story_locked. Re-vote to change scope.

List stories

GET /v1/sessions/{session_id}/stories — list stories in sort_order.

Terminal

Example response (200 OK)

JSON

Get a story

GET /v1/sessions/{session_id}/stories/{story_id} — fetch a single story.

Terminal

Add a story *

POST /v1/sessions/{session_id}/stories — append a story to the session. title is required.

Terminal

Example response (201 Created)

JSON

Update a story *

PATCH /v1/sessions/{session_id}/stories/{story_id} — partial update. Set final_estimate, edit scope (when not voted), reorder via sort_order.

Terminal

Example response (200 OK)

JSON

Delete a story *

DELETE /v1/sessions/{session_id}/stories/{story_id} — remove a story. Returns 204 No Content with no body.

Terminal

Voting & results

Read the votes cast on a story, its round history, and the computed consensus. These endpoints are read-only — casting votes stays in the app, where it is tied to a live participant. Votes are wiped when a new round starts, so votes always reflects the current round; the rounds history preserves each round's average and reveal time.

List current votes

GET /v1/sessions/{session_id}/stories/{story_id}/votes — every vote in the current round, oldest first.

Terminal

Example response (200 OK)

JSON

List round history

GET /v1/sessions/{session_id}/stories/{story_id}/rounds — each round with its average and revealed_at (null while a round is still open).

Terminal

Example response (200 OK)

JSON

Get consensus statistics

GET /v1/sessions/{session_id}/stories/{story_id}/results — aggregate stats for the current round: average, median, spread, distribution, and whether the numeric votes reached consensus.

Terminal

Example response (200 OK)

JSON

Participants

Participants reflect everyone who joined a session via the web app or chat integrations. Joining is initiated from the app — there is no POST on this endpoint, since identity in real-time sessions is established via the WebSocket join handshake. Internal identifiers (such as anonymous tokens or IP addresses) are never returned.

List participants

GET /v1/sessions/{session_id}/participants — return everyone who has joined the session, ordered by join time.

Terminal

Example response (200 OK)

JSON

Presence

presence is what the participant's browser last reported, and last_seen_at is when it reported it. Presence expires rather than being revoked — a browser can report that it is in the room but has no way to report that it stopped existing, so a participant whose heartbeat has gone quiet reads as offline even though no explicit leave was ever received.

ValueMeaning
onlineSession open in front of them and actively in use.
away Still connected, but the tab is in the background or has been idle for several minutes.
offline Left, or stopped reporting. last_seen_at may still be set — it records the last contact, not a promise they are present.

Poll if you need presence: there is no push delivery for it, and it is not carried in webhook payloads. participant.joined and participant.left cover membership changes, which is a different question from whether someone is currently looking at the room.

Get a participant

GET /v1/sessions/{session_id}/participants/{participant_id} — fetch a single participant from a session you organize.

Terminal

Remove a participant

DELETE /v1/sessions/{session_id}/participants/{participant_id} — remove a participant. The WebSocket room is notified and the participant leaves the session. Returns 204 No Content with no body. Removing the organizer returns 422 with code cannot_remove_organizer.

Terminal

Join requests & bans

Moderation endpoints are scoped under a session you organize. They are useful for approval-gated sessions and for clearing bans created from the app or API.

List pending join requests

GET /v1/sessions/{session_id}/join-requests returns pending join requests for sessions with require_approval enabled.

Terminal
JSON

Approve a join request

POST /v1/sessions/{session_id}/join-requests/approve accepts a pending request. The identifier is returned by the list endpoint and webhook payload.

Terminal
JSON

Deny a join request

POST /v1/sessions/{session_id}/join-requests/deny rejects a pending request. Send ban: true to also create a session ban for that requester.

Terminal
JSON

List bans

GET /v1/sessions/{session_id}/bans lists current bans for a session. Internal identifiers such as IP addresses and anonymous tokens are not returned.

Terminal
JSON

Remove a ban

DELETE /v1/sessions/{session_id}/bans/{ban_id} removes a ban and returns 204 No Content.

Terminal

Resolving a missing or expired join request returns 404 with join_request_not_found.

Account & credits

Inspect the account behind the API key — its plan, throttle ceiling, AI-data-consent flag, and AI credit balances — plus the personal AI credit ledger. Read account before attempting credit-gated or AI calls so you can surface a clear message when consent is off or the balance is empty.

Get the account

GET /v1/account — the key owner's plan and credits. No numeric user id is exposed; the API acts as a single account.

Terminal

Example response (200 OK)

JSON

PUT /v1/account/ai-consent with { "consent": true } records the account's acceptance of how story data is used by AI — the same consent the in-app modal captures, so a headless client can unblock AI without opening the app. Pass false to revoke. Until granted, every AI call returns 403 ai_consent_required.

Terminal

Example response (200 OK)

JSON

List the credit ledger

GET /v1/credits/transactions — newest first. Filter by type, action, from, and to. Page with limit (default 25, max 100) and before — pass the last row's id to fetch older entries.

Terminal

Example response (200 OK)

JSON

AI

Run the same on-demand AI actions the app offers — estimate suggestions, story explanations and enrichment, disagreement and session summaries, backlog ordering, and retrospectives — from your own tools. Every action costs 1 AI credit and never runs automatically.

Before you call

  • The account must have granted AI data consent — otherwise every AI call returns 403 with code ai_consent_required. Check ai_data_consent on GET /v1/account, and grant it (once) with PUT /v1/account/ai-consent ({ "consent": true }).
  • Each call spends a credit. When the balance can't cover it you get 402 with code insufficient_ai_credits (plus balance and cost).
  • Voter names and organizer notes are anonymized before any prompt leaves the server, then restored into your response.

Actions

ActionEndpointWho
Estimate suggestionPOST …/stories/{story_id}/ai/estimateOrganizer
Story explanationPOST …/stories/{story_id}/ai/explainParticipant
Disagreement summaryPOST …/stories/{story_id}/ai/disagreementOrganizer
Session summaryPOST …/sessions/{session_id}/ai/session-summaryParticipant
Story enrichment *POST …/sessions/{session_id}/ai/enrichOrganizer (paid)
Story ordering *POST …/sessions/{session_id}/ai/orderOrganizer (paid)
Retrospective narrative *POST …/sessions/{session_id}/ai/retro-narrativeOrganizer (paid)
Analytics insights *POST /analytics/ai/insightsOrganizer (paid)

Request & response envelope

Every action is a POST. Story/session actions take no body (the story and session in the URL are the input); the request bodies for the actions that need one are listed below. All actions return the same envelope — the action-specific payload is under result (and mirrored in the persisted output). Example, POST …/stories/{story_id}/ai/estimate:

Terminal

Example response (200 OK)

JSON

Request bodies

ActionBody
Story enrichment{ "title": "…", "source_metadata"?: { … } }
Story ordering{ "stories": [{ "key": "…", "title": "…", "description"?: "…" }] } (min 2)
Team sessions summary{ "session_ids": ["…"], "context_note"?: "…" } (1–25)
Team member insights{ "user_ids": [123], "context_note"?: "…" } (1–25)
All othersNo body

Result shape per action

The result object by action. Paid ("full") variants add the fields in italics.

Actionresult fields
Estimate suggestionestimate, rationale, confidence
Story explanationexplanation
Disagreement summarysummary, key_factors[], revote_focus
Session summarysummary, highlights[], next_steps[]
Story enrichmentdescription, acceptance_criteria[]
Story orderingorder[], rationale
Retrospective narrativenarrative, action_items[]
Analytics insightsnarrative, recommendations[]
Team sessions summarysummary, highlights[], per_session[], next_steps[]
Team member insightsmembers[] of { member, cadence, confidence_trend, tendencies[], notes_summary }

Read saved outputs (no credit)

Every AI result is persisted. Re-read the latest output per action — GET …/stories/{story_id}/ai-outputs for a story, or GET …/sessions/{session_id}/ai-outputs for session-level actions — to rehydrate prior results without spending a credit.

JSON

Teams

Manage the teams you belong to — list members and sessions, invite people, change roles, spin up a team session from the template, and run the shared AI credit pool. Access is scoped to your membership: a team you don't belong to returns 404, and admin-only actions return 403 with code forbidden for non-admin members. Teams bind by ULID. A member's id is a ULID (used in the member paths below); user_id is the account id you pass to the team AI member-insights action.

List your teams

GET /v1/teams — every team you're an active member of, with your role and counts.

Terminal

Example response (200 OK)

JSON

Get a team

GET /v1/teams/{team_id} — team detail including the AI pool and your viewer flags.

Terminal

Example response (200 OK)

JSON

List members (admin)

GET /v1/teams/{team_id}/members — active members first, then pending invites. Pending invites have a null user_id until the invitee signs in.

Terminal

Example response (200 OK)

JSON

Invite members (admin)

POST /v1/teams/{team_id}/members — up to 25 emails. Addresses are lower-cased and de-duplicated; anyone already on the team is skipped. Each new invite is emailed a join link. Returns only the newly created (pending) members.

Terminal

Example response (201 Created)

JSON

Change team role (admin)

PATCH /v1/teams/{team_id}/members/{member_id} — set role to admin or member. Changing the owner's role returns 422 with code team_owner_is_admin.

Terminal

Example response (200 OK)

JSON

Change session seat (admin)

PATCH /v1/teams/{team_id}/members/{member_id}/session-role — set session_role to organizer, voter, or spectator. A team has exactly one organizer: promoting a member demotes the previous organizer to voter. You can't directly demote the current organizer (promote someone else instead) — that returns 422 with code organizer_reassignment_required.

Terminal

Example response (200 OK)

JSON

Remove a member (admin)

DELETE /v1/teams/{team_id}/members/{member_id} — removes the member and revokes their seat in every team session. Returns 204 No Content. Removing the owner returns 422 with code cannot_remove_owner.

Terminal

List team sessions

GET /v1/teams/{team_id}/sessions — the team's sessions, most recently updated first, with participant / story / resolved counts.

Terminal

Example response (200 OK)

JSON

Create a team session

POST /v1/teams/{team_id}/sessions — build a session from the team template and auto-seat every member. Requires admin, or any member when the team allows member sessions (else 403 with code team_session_creation_forbidden). Returns the full session (same shape as the Sessions API).

Terminal

Example response (201 Created)

JSON

AI credit pool

GET /v1/teams/{team_id}/credits — the pool balance and whether it's enabled (any member).

Terminal

Example response (200 OK)

JSON

Pool ledger (admin)

GET /v1/teams/{team_id}/credits/transactions — newest first. Filter by type, action, from, to; page with limit (max 100) and before (a transaction id).

JSON

Fund, withdraw & toggle (admin)

POST …/credits/contribute moves your personal purchased credits into the pool; POST …/credits/withdraw moves them back — both take { "amount": N } (1–100000) and return the new pool state. Overdrawing returns 422 with code insufficient_credits and the available balance. POST …/credits/toggle takes { "enabled": true|false }.

Terminal

Example response (200 OK)

JSON

Team AI actions (admin)

Two admin-only AI actions spend the pool (or the admin's own credits if the pool can't cover it) and follow the same envelope as the AI section: POST /v1/teams/{team_id}/ai/sessions-summary ({ "session_ids": ["…"] }, 1–25) and POST …/teams/{team_id}/ai/member-insights ({ "user_ids": [123] }, 1–25 — the user_ids from the members list). Both accept an optional context_note.

Terminal

Webhooks

Webhooks let you react to planning session activity in real time. Configure endpoints from the API dashboard or the /webhook-endpoints API below, and Pokor will sign and POST event payloads to your URL whenever something happens in a session you organize.

Manage endpoints

Full CRUD lives under /v1/webhook-endpoints: GET lists them, POST creates one (returns the signing_secret in plaintext once), PATCH updates or toggles disabled, and DELETE removes one. The free plan allows one active endpoint; over the limit returns 403 with code webhook_quota_exceeded.

Create — POST /webhook-endpoints

Terminal

Example response (201 Created)

JSON

List — GET /webhook-endpoints

Your endpoints, newest first. The full secret is never returned again — only signing_secret_prefix.

JSON

Update / disable — PATCH /webhook-endpoints/{id}

Send only the fields you're changing: name, url, events, or disabled (a boolean that toggles delivery). A changed url is re-probed. Returns the updated endpoint under data.

Terminal

Delete — DELETE /webhook-endpoints/{id}

Removes the endpoint. Returns 204 No Content.

Terminal

Setup

  • Each endpoint stores an HTTPS destination URL and one or more event subscriptions.
  • Pokor probes new webhook URLs before saving them and expects a 2xx response within 10 seconds.
  • You may attach up to 5 custom headers to each delivery. Pokor-managed headers such as X-Pokor-Signature, X-Pokor-Timestamp, Content-Type, and User-Agent cannot be overridden.
  • The signing secret is shown exactly once when the endpoint is created.

Events

EventWhen it fires
participant.joinedA participant joined a planning session you organize.
participant.leftA participant left a planning session you organize.
participant.join_requested A participant requested access to an approval-gated session you organize.
story.votedA story received a vote.
story.revealed Voting cards were revealed — includes all votes and the average.
story.estimate_updatedThe organizer changed the final estimate after reveal.
session.disbandedA planning session was deleted by the organizer.

Delivery headers

HTTP

Payload structure

Every webhook payload uses the same envelope. The event_id is stable across retries for the same delivery, so use it to deduplicate idempotently.

JSON

Example: participant.joined

JSON

Example: participant.left

JSON

Example: participant.join_requested

JSON

Example: story.voted

JSON

Example: story.revealed

JSON

Example: story.estimate_updated

JSON

Example: session.disbanded

JSON

Retry policy

  • A delivery is successful when your endpoint returns a 2xx status within 10 seconds.
  • Organizer plan: up to 5 total attempts; retries at 1 min, 5 min, 30 min, and 2 h.
  • Free plan: up to 3 total attempts; retries at 1 min and 5 min.
  • After the final failure, the delivery is marked failed.
  • Delivery status, attempt count, response status, and last error are visible in the webhook dashboard.

Signature verification

The signature is sent as X-Pokor-Signature: v1=<hex_hmac_sha256>, with the unix timestamp in X-Pokor-Timestamp. Verify the raw request body, not a parsed and re-serialized JSON object. The signed payload is {timestamp}.{raw_body}. Reject timestamps outside a short replay window such as 5 minutes.

express.jsJavaScript

Playground

Pick an endpoint, paste an API key, fill in any path parameters and body, then send. Requests are sent directly to the public API and count against the pasted key's plan permissions, allowlists, and rate limits. Mutating calls (POST / PATCH / DELETE) require the safety toggle below.

API key

The key stays in this browser and is sent as Authorization: Bearer ....

Endpoint
Paste an API key to send a request.

Cookie preferences

Essential cookies keep Pokor working. Optional analytics only load if you say yes. Privacy policy