The complete REST endpoint reference. For tutorials and examples, see the API Guide.
Everything a project member can do in the web UI is available here — the SPA consumes this same API. Operations that require the manager role are marked (manager); everything else needs only project membership (or, for reads marked (viewer), any access level). The tables below name every route group the server mounts; the ones summarised in a single line are fully described in the live openapi.json.
https://eastagiletracker.com/api/v1https://api.eastagiletracker.com/api/v1 serves the identical API. All requests and responses are JSON, except a few file-upload endpoints that accept multipart.
Two groups sit one level up, under /api rather than /api/v1: the authentication surface (/api/auth/*) and the public forms (/api/contact, /api/feedback). Their /api/v1/… spellings return 404.
Authentication
Section titled “Authentication”Every authenticated request sends a credential via one of:
X-TrackerToken: <key>Authorization: Bearer <key>
User keys start with ea_user_, agent keys with ea_agent_, and MCP access tokens with ea_mcp_. See API Guide → Three kinds of credentials.
Unauthenticated endpoints: /openapi.json, /docs, the /api/auth/* endpoints, and the reference-data lookups (/story_types, /story_states, /effort_scales, /priority_scales). /meta is authenticated — any valid key works, but it isn’t project-scoped (a project-bound agent key reaches it too).
Four levels gate project-scoped endpoints:
| Level | Who passes | Typical operations |
|---|---|---|
| public viewer | anyone, on a project whose visibility is public | reads of the board: stories, iterations, search, story and epic activity (with actor details redacted) |
| viewer | viewer, member, manager | reads (list/get stories, search, metrics, export format list) |
| member | member, manager | all work-item writes (stories, tasks, comments, …), the event stream |
| manager | manager only | project settings, membership management, agent keys, delete, import, export downloads, backups, audit log |
Agents hold the same roles as members — viewer, member, or manager — capped at the role of the member who minted the key. A non-member receives 404 unfound_resource (not 403) on private project paths, so project IDs aren’t enumerable.
Self-describing endpoints
Section titled “Self-describing endpoints”| Method | Path | Description |
|---|---|---|
| GET | /openapi.json | The live OpenAPI 3 spec, request bodies included. Unauthenticated. |
| GET | /docs | Swagger UI. Unauthenticated. |
| GET | /meta | Caller identity (auth.kind/key_id/agent_id/project_id) + the story-type transition graph. Authenticated (any valid key; not project-scoped). Call this first. |
| GET | /api/health · /api/config | Liveness, and the deployment’s public configuration (single-org mode, which optional features are on, the instance name). Unauthenticated, outside /v1. |
Auth (/api/auth/*, outside /v1)
Section titled “Auth (/api/auth/*, outside /v1)”Session endpoints, unauthenticated unless noted. The SPA drives these; scripts normally use an API key instead.
| Method | Path | Description |
|---|---|---|
| POST | /auth/register | Register a new account — reCAPTCHA-guarded; the account then passes the SMS challenge |
| POST | /auth/sms/challenge · /auth/sms/status · /auth/sms/bypass | Send / check the sign-up SMS code (bypass is operator-gated) |
| GET | /auth/config | Which sign-in methods the deployment offers |
| POST | /auth/login | Sign in with email + password; returns a session JWT, or a TOTP challenge |
| POST | /auth/login/totp | Finish a sign-in with an authenticator code or a recovery code |
| POST | /auth/passkey/login/start · /auth/passkey/login/finish | Passwordless WebAuthn sign-in |
| GET | /auth/oauth/github/start · /auth/oauth/github/callback · /auth/oauth/google/start · /auth/oauth/google/callback · POST /auth/oauth/exchange | OAuth sign-in with GitHub or Google |
| POST | /auth/refresh · /auth/refresh/revoke | Rotate the refresh token / revoke it |
| POST | /auth/logout | Sign out (revokes the refresh token) |
| POST | /auth/forgot-password · /auth/reset-password | Request a reset email / use the reset token |
| POST | /auth/accept-invite/lookup · /auth/accept-invite | Resolve an invitation token → email / accept the project invitation (after authenticating) |
Account / identity
Section titled “Account / identity”These act on the caller and need only a valid key (no project role).
| Method | Path | Description |
|---|---|---|
| GET | /me | Current user profile |
| PUT | /me | Update profile |
| DELETE | /me | Delete account — refused while you are the sole owner of an org or of a project with other members |
| GET | /me/deletion-impact | What deleting the account would remove and what blocks it |
| PUT | /me/password | Change password |
| PUT | /me/settings | Update settings (theme, notification preferences) |
| POST | /me/avatar | Upload avatar (multipart) |
| POST | /me/api-token/regenerate | Rotate your API token — invalidates existing sessions/keys |
| GET | /me/api_keys · POST /me/api_keys · DELETE /me/api_keys/{id} | Manage user (ea_user_) API keys |
| GET | /me/totp · POST /me/totp/setup · /me/totp/verify · /me/totp/disable | Two-factor (TOTP) enrollment; verify returns the recovery codes once |
| GET | /me/passkeys · POST /me/passkeys/register/start · /me/passkeys/register/finish · DELETE /me/passkeys/{credential_id} | Passkey enrollment and removal |
| GET | /me/oauth_grants · DELETE /me/oauth_grants/{grant_id} | Connected apps — the MCP clients and OAuth apps you’ve authorized |
| GET | /me/activity | Your activity across all projects |
| GET | /me/stories | Stories you own, requested, or follow across every project the token can reach — role=owned|requested|following, state=, cursor= / limit= (max 200) |
| GET | /me/mentions · POST /me/mentions/{mention_id}/ack | The @-mention inbox (unacked=true to filter) and acknowledgement — also folded into the notification feed below |
| GET | /me/data-export | GDPR self-export of your data |
| GET | /me/consent · POST /me/consent | Read / record consent ({ consent_type, granted }) |
| GET | /legal/pending · POST /legal/accept | Pending clickwrap docs / record acceptance |
| GET / PUT | /agent/me | An agent key’s own identity and profile, readable and editable by the agent (the agent-side counterpart of /me) |
| POST | /api/contact · /api/feedback · /api/feedback/with-screenshot | Contact + in-app feedback. Outside /v1; rate-limited per IP |
Reference data (unauthenticated)
Section titled “Reference data (unauthenticated)”Seed lookups used when creating/estimating stories. Stable IDs.
| Method | Path | Description |
|---|---|---|
| GET | /story_types | feature, bug, chore, release (+ allow_points) |
| GET | /story_states | unstarted … accepted, rejected |
| GET | /effort_scales | available estimate scales |
| GET | /effort_scales/{scale_id}/values | the point values in a scale |
| GET | /priority_scales · /priority_scales/{scale_id}/values | the priority scales and their values (priority_id on a story resolves here) |
Organizations
Section titled “Organizations”Hosted service only — a self-hosted install runs in single-organization mode and does not mount these (except the org list). Roles are org roles: owner, admin, member.
| Method | Path | Description |
|---|---|---|
| GET / POST | /organizations | List your organizations / create one |
| GET / PUT / DELETE | /organizations/{oid} | Read, rename (name + slug; owner or admin), delete |
| GET / POST | /organizations/{oid}/memberships · PUT / DELETE …/memberships/{member_id} | Members and invitations; invites carry a role ceiling (never above the caller’s; owner is never invited) |
| POST | /organizations/{oid}/memberships/bulk-role · …/memberships/bulk-remove | Change the role of, or remove, up to 200 members at once. All-or-nothing: a batch that would remove the last owner or leave a project without an owner is refused whole; with reassign_confirmed you become owner of those projects instead |
| DELETE | /organizations/{oid}/invitations/{invitation_id} | Revoke a pending invitation |
| POST | /organizations/{oid}/transfer-ownership | Hand the owner role to another member |
| PUT | /organizations/{oid}/memberships/{member_id}/anonymization | Mask a member’s name / email / avatar org-wide |
| GET | /organization-invitations/{token} · POST …/{token}/accept | Resolve / accept an emailed org invitation |
| POST | /organizations/{oid}/export · GET / POST …/export/jobs · GET …/export/jobs/{job_id} · …/export/jobs/{job_id}/download | Owner-only org export: a zip with a SQL dump and every attachment, run as a job |
Projects
Section titled “Projects”| Method | Path | Description |
|---|---|---|
| GET | /projects | List your projects (limit ≤ 200) |
| POST | /projects | Create a project |
| GET | /projects/{id} | Get project details (viewer) |
| PUT | /projects/{id} | Update project settings (manager) |
| DELETE | /projects/{id} | Delete a project (manager) |
| POST | /projects/{id}/pin | Pin / unpin the project on your project list |
| POST | /projects/{id}/transfer-organization | Move the project to another organization (manager) |
| POST | /projects/{id}/slack/test | Send a test message to the project’s Slack feed (manager) |
| GET / POST | /projects/{id}/showcase_claim_eligibility · /projects/{id}/showcase_claim · /projects/{id}/showcase_seed | Public showcase projects: check whether you can claim one, claim it, seed it |
| GET | /projects/{id}/audit-log | Audit-log read — project history plus per-story / per-epic activity via surface=; access varies by surface, see below |
| GET | /projects/{id}/events | Cursor-paginated event stream (member) — see Events |
Audit-log query parameters: event_type= (single or comma-separated list), limit= (≤ 1000), before= (keyset cursor, ISO-8601 created_at), surface= (project_history, story_activities, epic_activities), target_id= (the story/epic id — required when surface=story_activities or epic_activities). Access: the unfiltered log and surface=project_history are (manager); story_activities / epic_activities are readable by any project member, and anonymously on public projects with actor PII redacted.
Members, agents, and agent keys
Section titled “Members, agents, and agent keys”| Method | Path | Description |
|---|---|---|
| GET | /projects/{id}/memberships | List members (viewer) |
| POST | /projects/{id}/memberships | Invite a member by email (manager) |
| PUT | /projects/{id}/memberships/{mid} | Update role (manager) |
| DELETE | /projects/{id}/memberships/{mid} | Remove a member (manager) |
| GET | /projects/{id}/addable-members · POST /projects/{id}/members/add-existing | Org members not yet on the project / add one without an email invite (manager) |
| POST | /projects/{id}/members/join | An org owner or admin joins a project in their org as a manager, or promotes themselves to one (the Make me owner action on the project list) |
| PUT | /projects/{id}/members/{mid}/anonymization | Mask a member’s name / email / avatar on this project (manager) |
| GET / POST | /projects/{id}/agent_keys | List / mint agent keys — managers, or the roles the project’s creator-roles policy admits |
| DELETE | /projects/{id}/agent_keys/{kid} | Revoke an agent key |
| GET | /projects/{id}/agent_keys/onboarding | The onboarding bundle: prompts and config files for the common agent clients |
| GET | /projects/{id}/agents · GET / PUT /projects/{id}/agents/{aid} | The project’s agents and their profiles (name, initials, description, colour) |
| POST | /projects/{id}/agents/{aid}/rotate-key · /projects/{id}/agents/{aid}/avatar | Rotate an agent’s key (identity and history kept) / upload its avatar |
Stories
Section titled “Stories”All story writes need the member role.
| Method | Path | Description |
|---|---|---|
| GET | /projects/{id}/stories | List stories (paginated, filterable) (viewer) |
| POST | /projects/{id}/stories | Create a story |
| GET | /projects/{id}/stories/{sid} | Get one story (viewer) |
| PUT | /projects/{id}/stories/{sid} | Update a story |
| DELETE | /projects/{id}/stories/{sid} | Delete a story |
| POST | /projects/{id}/stories/{sid}/transitions | Change state with validation |
| POST | /projects/{id}/stories/{sid}/reject · /projects/{id}/stories/{sid}/restart | Reject a delivered story / put a rejected one back to started (rejected is terminal for /transitions) |
| POST | /projects/{id}/stories/{sid}/archive · /projects/{id}/stories/{sid}/unarchive | Archive / unarchive one story |
| POST | /projects/{id}/stories/bulk_transition | Transition many stories (1–100) at once |
| POST | /projects/{id}/stories/bulk-archive · bulk-delete · bulk-duplicate · bulk-move | Archive, delete, duplicate, or move (to a panel / position) many stories |
| POST | /projects/{id}/stories/{sid}/duplicate | Duplicate one story |
| GET / POST / DELETE | /projects/{id}/stories/{sid}/epics · …/epics/{eid} | The story’s epic membership |
| GET | /short-links/{code} · /story-references | Resolve a /s/<code> short link to its story / resolve up to 100 story references (#id, URLs) to the stories the caller can read |
Story-list query parameters: archived= (exclude default / include / only — the tri-state archived filter; supersedes the deprecated include_archived=true, which now aliases archived=include), include_done=true (admits Done-panel stories frozen on past iterations, excluded by default). Pagination (cursor= / limit= / offset=) and sparse fieldsets (fields=) follow Pagination and Field projection.
Create (POST …/stories): { "name" (required), "story_type": "feature|bug|chore|release", "description"?, "estimate"?, "current_state"?, "icebox"?, "labels"? }. estimate is the scale value’s label as a string ("3", "13"); a JSON number is rejected. labels accepts ["auth"] or [{ "name": "auth" }]; unknown labels are created. Defaults: story_type=feature, current_state=unstarted.
Update (PUT …/stories/{sid}): same fields, all optional, plus "position" (float), "force_state_change" (bool), and "expected_updated_at" (RFC 3339 — a description save is refused with 409 stale_write if the story changed since you read it). Story writes also honour If-Match against the story’s ETag; a mismatch is 412 precondition_failed.
Transition (POST …/transitions): { "to": "<state>" }. The field is to. Returns { story_id, state }. Illegal move → 422 invalid_transition with details: { from, to, allowed }.
Bulk transition (POST …/bulk_transition): { "story_ids": [int,…] (1–100), "to": "<state>" }. Each story is judged independently; returns { results: [ { id, status: "ok" } | { id, status: "failed", error } ] }.
Story sub-resources
Section titled “Story sub-resources”All member. List/GET on most is (viewer).
| Method | Path | Body / notes |
|---|---|---|
| GET / POST | /projects/{id}/stories/{sid}/tasks · PUT/DELETE …/tasks/{tid} | { description (or task_desc), complete?, task_order? } |
| GET / POST | /projects/{id}/stories/{sid}/comments · PUT/DELETE …/comments/{cid} | { text (or comment_text) } or { comment_emoji }. GET takes fields= (allowlist: comment_id, story_comment_id, story_id, comment_text, comment_emoji, member, created) plus cursor= / limit= (≤ 200) / order=asc|desc |
| GET / POST | /projects/{id}/stories/{sid}/blockers · PUT/DELETE …/blockers/{bid} | { blocker_desc, resolved? } |
| GET / POST | /projects/{id}/stories/{sid}/links · PUT/DELETE …/links/{lid} | { url, link_type?, title? } — link_type ∈ relates_to, duplicates, blocks, is_blocked_by, pull_request, branch, other; GitHub /pull/ and /tree/ URLs are typed automatically |
| GET / POST | /projects/{id}/stories/{sid}/reviews · PUT/DELETE …/reviews/{rid} | Create: { reviewer_id? / reviewer_agent_id?, comment? } — omit both to assign yourself. Update: { status, comment? } |
| GET / POST | /projects/{id}/stories/{sid}/owners · DELETE …/owners/{mid} · DELETE …/owners/agents/{aid} | { member_id? / agent_id? } — omit both to add the caller |
| GET / POST | /projects/{id}/stories/{sid}/followers · DELETE …/followers/{mid} · DELETE …/followers/agents/{aid} | { member_id? / agent_id? } |
| GET / POST | /projects/{id}/stories/{sid}/labels · DELETE …/labels/{lid} | { name } |
| GET / POST | /projects/{id}/stories/{sid}/attachments (+ /json) · DELETE …/attachments/{aid} | multipart upload — video ≤ 200 MB, PDF / Word / Excel ≤ 25 MB, images / CSV / text ≤ 10 MB; list is (viewer) |
| GET / POST | /projects/{id}/stories/{sid}/link-attachments · DELETE …/link-attachments/{laid} | Link attachments — an external URL kept alongside the file attachments rather than as a code link |
| GET | /attachments/{token} · /api/avatars/{token} | Token-addressed reads of an attachment or an avatar — the URLs the API hands out; no X-TrackerToken needed |
Same shape as stories, minus the state machine. member for writes, (viewer) for reads.
| Method | Path | Description |
|---|---|---|
| GET / POST | /projects/{id}/epics · GET / PUT / DELETE …/epics/{eid} | Epics carry a name, a Markdown description, and a backing label that joins their stories |
| GET / POST / PUT / DELETE | …/epics/{eid}/comments · …/comments/{cid} | Epic comments |
| GET / POST / DELETE | …/epics/{eid}/owners · …/followers (+ /agents/{aid} variants) | Owners and followers, members or agents — an epic’s owners propagate to its stories |
| GET / POST / DELETE | …/epics/{eid}/attachments (+ /json) · …/link-attachments | Attachments, same caps as stories |
| GET | /projects/{id}/analytics/epics · …/analytics/epics/{eid} | Per-epic progress: burnup, throughput, health, forecast (viewer) |
Labels
Section titled “Labels”member for writes, (viewer) for reads.
| Method | Path | Description |
|---|---|---|
| GET / POST | /projects/{id}/labels | List / create a label |
| PUT / DELETE | /projects/{id}/labels/{lid} | Update / delete a label |
| POST | /projects/{id}/labels/{lid}/archive | Archive (soft-hide) a label |
Iterations
Section titled “Iterations”Reads are open to any project role, and anonymous on a public project.
| Method | Path | Description |
|---|---|---|
| GET | /projects/{id}/iterations | List iterations (≤ 500 per page; carries an ETag and the X-Tracker-Pagination-* continuation headers when truncated) |
| GET | /projects/{id}/iterations/{itid} | One iteration |
| GET | /projects/{id}/iterations/first-preview | The dates the first iteration would get, shown in the seeding confirmation |
| POST | /projects/{id}/iterations | Create a manual iteration (member) |
| DELETE | /projects/{id}/iterations/{itid} | Delete an iteration (manager) |
| PUT | /projects/{id}/iterations/{itid}/velocity | Override one iteration’s velocity without changing the project strategy (manager) |
| GET | /projects/{id}/iterations/{itid}/done-stories | The accepted stories of a closed iteration, paginated |
Search, metrics, preferences
Section titled “Search, metrics, preferences”| Method | Path | Description |
|---|---|---|
| GET | /projects/{id}/search?q=… | Powerful search — full-text + facet / date-range / people qualifiers (GitHub-style DSL); returns { results, total, limit, offset }. query aliases q; limit= (default 50, max 1000) / offset= paginate; sort= orders by relevance (default), created, created_asc, state, or updated. (viewer) — see the Guide |
| GET | /projects/{id}/metrics/{velocity,burndown,story-types,contributors} | The Metrics page’s series (viewer); epic metrics are under /analytics/epics above |
| GET | /projects/{id}/backlog/grouping | The Backlog’s projected iteration groups (viewer) |
| GET / PUT | /projects/{id}/preferences | Your board preferences for this project — any project role, your own row only |
Events
Section titled “Events”| Method | Path | Description |
|---|---|---|
| GET | /projects/{id}/events | Cursor-paginated event stream (member) — viewers get 403 |
Query parameters: since=<event_id>, types=story.created,story.transitioned,comment.added,…, limit= (≤ 500), cursor=. Response includes next_cursor. Pass the last event_id you saw as since to resume.
Notifications
Section titled “Notifications”The unified in-app notification feed: first-class notification rows (review requests, story activity, invitations, …) merged with the @-mention inbox into one newest-first stream. Feed ids are source-prefixed (nt-… / sc-… / ec-…). Member sessions and ea_user_* keys read their member-side rows; ea_agent_* keys their agent-side rows.
| Method | Path | Description |
|---|---|---|
| GET | /me/notifications | Your notification feed. Filters: unread=true, since_id=, kind= (mentions / reviews / stories / invitations); paginate with cursor= / limit= |
| GET | /me/notifications/unread-count | Unread totals — { unread_count, by_category: { mentions, reviews, stories, invitations } } |
| POST | /me/notifications/read-all | Mark everything read; returns the fresh counts |
| POST | /me/notifications/{id}/ack | Mark one item read (idempotent) |
| POST | /me/notifications/{id}/accept | Accept a project / organization invitation from the feed (member tokens only) |
| POST | /me/notifications/{id}/decline | Decline a project / organization invitation (member tokens only) |
| GET | /me/notifications/resolve-invite?token=… | Map an emailed invite token to your notification id — { "id": "nt-…" } or { "id": null } |
| GET | /me/notifications/stream | Live push — Server-Sent Events (text/event-stream); see below |
The stream endpoint is not a JSON endpoint and is therefore not in the OpenAPI spec: it holds the connection open and emits a payload-less frame ({"type":"notification","kind":…}) whenever something new lands, telling the client to refetch the feed. Connections are capped server-side at 45 minutes — reconnect and re-authenticate. Member sessions and ea_user_* keys only; ea_agent_* keys get 403.
Import (manager)
Section titled “Import (manager)”| Method | Path | Description |
|---|---|---|
| POST | /projects/{id}/import | File sources: source= ∈ pivotal, jira, asana, gitlab, shortcut, trello, linear, plane, plane_json, eat. Multipart file=. Synchronous — answers with the result counts. |
| POST | /projects/{id}/import/json | JSON body; source=github needs no file — owner, repo, optional token, and the opt-in flags include_pull_requests / include_milestones / include_releases / include_dependencies; the file sources send file_base64. Asynchronous: returns 202 { import_id, status }. The server fetches over GitHub’s GraphQL API, which rejects anonymous callers, so a token always reaches GitHub — yours, or the deployment’s shared one. See the Guide. |
| GET | /projects/{id}/imports/{import_id} | Poll a job: status runs pending → fetching → writing → done | failed, with progress_current / progress_total while fetching and the result counts on done |
One import runs per project at a time; a second POST while one is in flight is 409 import_already_running. dry_run: true (JSON body or dry_run=true multipart) previews any source: parses, resolves, de-dupes, produces the same { imported, skipped, errors, unmatched } counts, then rolls back — nothing is written. Caps: 10 MiB body, and 5,000 stories per import for the file-based sources (over either → 400, nothing written). The GitHub source is uncapped — it commits in chunks rather than one transaction. Re-import is idempotent per source id — already-imported rows are skipped, not duplicated.
Export
Section titled “Export”| Method | Path | Description |
|---|---|---|
| GET | /projects/{id}/export/formats | Registered formats: { id, name, content_type, drops, includes_archived }. Any project role. |
| GET | /projects/{id}/export/{format} | Download one (manager). Interchange: eat (full fidelity), jira, pivotal, shortcut, trello, asana, gitlab, linear, plane, plane_json; documents: pdf, docx. |
| GET | /projects/{id}/export/attachments | Every attachment as one browsable zip (files keep original names; JSON + CSV manifest) (manager). |
Document exports (pdf, docx) take extra query parameters: page_size= (letter default / a4 / legal / folio), from= / to= (story-window bounds — RFC 3339 or bare YYYY-MM-DD; a story is in range when its created or completed_at falls inside it), include_icebox= / include_backlog= (both default false, so a shareable export shows only scheduled / in-flight work). The interchange CSV formats ignore them.
Backups and restores (manager)
Section titled “Backups and restores (manager)”| Method | Path | Description |
|---|---|---|
| GET / POST | /projects/{id}/backups · GET …/backups/{snapshot_id} · …/backups/health | List snapshots, take one now, read one, and the retention health summary |
| GET / POST | /projects/{id}/restores · POST …/restores/partial · GET …/restores/{restore_id} | Restore a whole snapshot, or selected tables from one, and poll the restore |
The POSTs sit on the sensitive rate-limit tier (below).
MCP and OAuth provider
Section titled “MCP and OAuth provider”East Agile Tracker is an OAuth 2.1 provider for MCP clients. A client discovers it at /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource/mcp, sends you to /oauth/authorize (the consent page), exchanges the code at /oauth/token, and then speaks MCP at /mcp with the resulting ea_mcp_* token. Grants are listed and revoked at /me/oauth_grants. The provider endpoints have their own rate-limit tier.
WebSocket
Section titled “WebSocket”wss://eastagiletracker.com/ws/control?token=<session JWT>For interactive UI remote-control ({ "action": "get_state", "id": "req-1" }). The token is a browser session JWT — an API key is refused with 401 before the upgrade. Not a data channel — all reads/writes go through REST. Single-instance only; not fanned out across replicas.
Idempotency
Section titled “Idempotency”Write endpoints (POST, PUT, DELETE) accept an Idempotency-Key header. Same key + same body replays the cached response (24-hour window); same key + a different body returns 409 idempotency_conflict. The key is scoped to the credential that sent it. Not applied to GET/HEAD/OPTIONS, /openapi.json and /docs, /api/auth/*, or multipart uploads on /attachments paths. Responses that stopped short of a domain answer are never cached — 401, 403, 404, 429, and every 5xx — so a retry after any of them reaches the handler; 400, 409, 412, and 422 are the domain’s answer and replay like a success.
Pagination
Section titled “Pagination”List endpoints accept cursor=<opaque> and limit=<n>. When set, the response is { "items": [...], "next_cursor": "<str|null>" }; pass next_cursor back to page. The limit cap is per endpoint: 200 on stories, comments, and projects; 500 on events; 1000 on search and the audit log.
A plain list (no cursor/limit) that had to truncate its response says so in headers — X-Tracker-Pagination-Truncated, X-Tracker-Pagination-Limit, X-Tracker-Pagination-Offset, and X-Tracker-Pagination-Next-Offset; pass the last one back as offset= for the next page. There is no total-count header.
Field projection
Section titled “Field projection”List endpoints accept fields= (comma-separated) to return only specific fields. story_id is always included; an unknown field name returns 400 validation_failed with the offending names in details.fields.
GET /projects/123/stories?fields=story_id,name,current_state,ownersError format
Section titled “Error format”Every JSON error has code and error; some add details:
{ "code": "invalid_transition", "error": "Cannot move story from `unstarted` to `accepted`", "details": { "from": "unstarted", "to": "accepted", "allowed": ["started"] } }| Status | code | When |
|---|---|---|
| 400 | invalid_parameter | bad input; message in error, no details (most validation: blank/length/null-byte/email) |
| 400 | validation_failed | structured input error; details.fields is an array of offending field names |
| 401 | unauthenticated | missing/invalid token |
| 403 | unauthorized_operation | authenticated but insufficient role |
| 404 | unfound_resource | not found — also returned to non-members |
| 409 | conflict | resource conflict (e.g. duplicate) |
| 409 | idempotency_conflict | Idempotency-Key reused with a different body |
| 409 | stale_write · import_already_running | the story changed since your expected_updated_at · an import is already in flight |
| 412 | precondition_failed | If-Match didn’t match the resource’s current ETag; details carries expected and current |
| 413 | request_too_large | the body exceeds the route’s size limit |
| 422 | invalid_transition | illegal state move; details carries { from, to, allowed } |
| 429 | rate_limited | too many requests from this IP on a rate-limited route; Retry-After header |
| 500 | internal_error | server fault — generic message; safe to retry |
| 503 | not_configured | the deployment lacks the integration this route needs (SMS, object storage, …) |
details.fields is a JSON array of field names (e.g. ["to"]), sometimes with extra keys like max. There is no field→message map.
{ "code": "validation_failed", "error": "unknown field(s): foo", "details": { "fields": ["foo"] } }Rate limits
Section titled “Rate limits”Per client IP, on a handful of routes; authenticated API traffic elsewhere is not rate-limited. Defaults (each pair is sustained rate and burst, operator-tunable):
- Auth —
/api/auth/*: 0.5 req/s, burst 20. - OAuth provider —
/oauth/*: 1 req/s, burst 60. - Public —
/api/contact: 0.2 req/s, burst 10. - Feedback —
/api/feedback: three stacked tiers — one submit per 15 s, 10 per hour, 36 per day. - Avatars — the unauthenticated avatar redirect: 20 req/s, burst 200.
- Sensitive — the backup and restore
POSTs: ~0.002 req/s, burst 5.
An exceeded limit returns 429 with a Retry-After header and the standard JSON error envelope, code: "rate_limited".