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 owner role are marked (owner); everything else needs only project membership (or, for reads marked (viewer), any access level).
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.
Authentication
Section titled “Authentication”Every authenticated request sends an API key via one of:
X-TrackerToken: <key>Authorization: Bearer <key>
User keys start with ea_user_. Agent keys start with ea_agent_. See API Guide → Two kinds of keys.
Unauthenticated endpoints: /openapi.json, /docs, the /auth/* endpoints, and the reference-data lookups (/story_types, /story_states, /effort_scales, …). /meta is authenticated — any valid key works, but it isn’t project-scoped (a project-bound agent key reaches it too).
Three levels gate project-scoped endpoints:
| Level | Who passes | Typical operations |
|---|---|---|
| viewer | viewer, member, owner | reads (list/get stories, search, analytics) |
| member | member, owner | all work-item writes (stories, tasks, comments, …) |
| owner | owner only | project settings, membership management, agent keys, delete, import, audit log |
A non-member receives 404 unfound_resource (not 403) on 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. 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. |
Account / identity
Section titled “Account / identity”These act on the caller and need only a valid key (no project role).
| Method | Path | Description |
|---|---|---|
| POST | /auth/register | Register a new account |
| POST | /auth/login | Sign in, returns a session token |
| POST | /auth/logout | Sign out |
| POST | /auth/forgot-password | Request a password-reset email |
| POST | /auth/reset-password | Use a reset token to set a new password |
| GET | /auth/verify-email | Verify an email address |
| POST | /auth/accept-invite/lookup | Resolve an invitation token → email (unauthenticated) |
| POST | /auth/accept-invite | Accept a project invitation (after authenticating) |
| GET | /me | Current user profile |
| PUT | /me | Update profile |
| DELETE | /me | Delete account |
| 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/activity | Your activity across all projects |
| 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 |
| POST | /contact · POST /feedback · POST /feedback/with-screenshot | Contact + in-app feedback |
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 |
Projects
Section titled “Projects”| Method | Path | Description |
|---|---|---|
| GET | /projects | List your projects |
| POST | /projects | Create a project |
| GET | /projects/{id} | Get project details (viewer) |
| PUT | /projects/{id} | Update project settings (owner) |
| DELETE | /projects/{id} | Delete a project (owner) |
| GET | /projects/{id}/audit-log | Project activity stream (owner) |
| GET | /projects/{id}/events | Cursor-paginated event stream (viewer) — see Events |
Members and agent keys
Section titled “Members and agent keys”| Method | Path | Description |
|---|---|---|
| GET | /projects/{id}/memberships | List members (viewer) |
| POST | /projects/{id}/memberships | Invite a member by email (owner) |
| PUT | /projects/{id}/memberships/{mid} | Update role (owner) |
| DELETE | /projects/{id}/memberships/{mid} | Remove a member (owner) |
| GET / POST | /projects/{id}/agent_keys | List / mint agent keys (owner) |
| DELETE | /projects/{id}/agent_keys/{kid} | Revoke an agent key (owner) |
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/bulk_transition | Transition many stories (1–100) at once |
| POST | /projects/{id}/stories/bulk-delete | Delete many stories |
| POST | /projects/{id}/stories/bulk-duplicate | Duplicate many stories |
| POST | /projects/{id}/stories/{sid}/duplicate | Duplicate one story |
Create (POST …/stories): { "name" (required), "story_type": "feature|bug|chore|release", "description"?, "estimate"?, "current_state"?, "icebox"?, "labels"? }. 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) and "force_state_change" (bool).
Transition (POST …/transitions): { "to": "<state>", "reason"? }. 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>", "reason"? }. 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 / POST | /projects/{id}/stories/{sid}/blockers · PUT/DELETE …/blockers/{bid} | { blocker_desc, resolved? } |
| GET / POST | /projects/{id}/stories/{sid}/links · DELETE …/links/{lid} | { url, link_type?, title? } |
| GET / POST | /projects/{id}/stories/{sid}/reviews · PUT/DELETE …/reviews/{rid} | { reviewer_id? / reviewer_agent_id?, 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 (≤ 2 GB each); list is (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”| Method | Path | Description |
|---|---|---|
| GET | /projects/{id}/iterations | List iterations (member) |
| POST | /projects/{id}/iterations | Create a manual iteration (owner) |
| DELETE | /projects/{id}/iterations/{itid} | Delete an iteration (owner) |
Search, analytics, metrics, preferences
Section titled “Search, analytics, metrics, preferences”| Method | Path | Description |
|---|---|---|
| GET | /projects/{id}/search?query=… | Filter syntax search (viewer) — see Guide |
| GET | /projects/{id}/analytics/{overview,iteration,releases,activity,cycle-time,projections} | Analytics (viewer). Iteration drilldown takes ?iteration_id= |
| GET | /projects/{id}/metrics/{velocity,burndown,story-types} | Metrics (viewer) |
| GET / PUT | /projects/{id}/preferences | Your board preferences for this project (member) |
Events
Section titled “Events”| Method | Path | Description |
|---|---|---|
| GET | /projects/{id}/events | Cursor-paginated event stream (viewer) |
Query parameters: since=<event_id>, types=story.created,story.transitioned,comment.created,…, limit=, cursor=. Response includes next_cursor. Pass the last event_id you saw as since to resume.
Import (owner)
Section titled “Import (owner)”| Method | Path | Description |
|---|---|---|
| POST | /projects/{id}/import (+ /json) | Multipart upload. source= ∈ pivotal, jira, asana, gitlab, shortcut, trello, linear, plane, plane_json. |
WebSocket
Section titled “WebSocket”wss://eastagiletracker.com/ws/control?token=<key>For interactive UI remote-control ({ "action": "get_state", "id": "req-1" }). 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. Not applied to GET/HEAD/OPTIONS, /auth/*, or /attachments paths. 5xx responses are never cached — a retry reaches the handler.
Pagination
Section titled “Pagination”List endpoints accept cursor=<opaque> and limit=<n≤200>. When set, the response is { "items": [...], "next_cursor": "<str|null>" }; pass next_cursor back to page. Some lists also return an X-Tracker-Pagination-Total 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 |
| 422 | invalid_transition | illegal state move; details carries { from, to, allowed } |
| 500 | internal_error | server fault — generic message; safe to retry |
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-key (per-IP for unauthenticated endpoints), GCRA token bucket:
- Auth —
/auth/*: 0.5 req/s, burst 20. - Public —
/contact,/feedback: 0.2 req/s, burst 10. - Sensitive — password-reset: ~0.002 req/s, burst 5.
An exceeded limit returns 429 Too Many Requests with a Retry-After header and a plain-text body (not the JSON error envelope).