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

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

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.

Webhooks

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

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 and external widgets only load if you say yes. Privacy policy