Skip to content

API Specification

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/v1

https://api.eastagiletracker.com/api/v1 serves the identical API. All requests and responses are JSON, except a few file-upload endpoints that accept multipart.

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:

LevelWho passesTypical operations
viewerviewer, member, ownerreads (list/get stories, search, analytics)
membermember, ownerall work-item writes (stories, tasks, comments, …)
ownerowner onlyproject 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.

MethodPathDescription
GET/openapi.jsonThe live OpenAPI 3 spec. Unauthenticated.
GET/docsSwagger UI. Unauthenticated.
GET/metaCaller identity (auth.kind/key_id/agent_id/project_id) + the story-type transition graph. Authenticated (any valid key; not project-scoped). Call this first.

These act on the caller and need only a valid key (no project role).

MethodPathDescription
POST/auth/registerRegister a new account
POST/auth/loginSign in, returns a session token
POST/auth/logoutSign out
POST/auth/forgot-passwordRequest a password-reset email
POST/auth/reset-passwordUse a reset token to set a new password
GET/auth/verify-emailVerify an email address
POST/auth/accept-invite/lookupResolve an invitation token → email (unauthenticated)
POST/auth/accept-inviteAccept a project invitation (after authenticating)
GET/meCurrent user profile
PUT/meUpdate profile
DELETE/meDelete account
PUT/me/passwordChange password
PUT/me/settingsUpdate settings (theme, notification preferences)
POST/me/avatarUpload avatar (multipart)
POST/me/api-token/regenerateRotate 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/activityYour activity across all projects
GET/me/data-exportGDPR self-export of your data
GET/me/consent · POST /me/consentRead / record consent ({ consent_type, granted })
GET/legal/pending · POST /legal/acceptPending clickwrap docs / record acceptance
POST/contact · POST /feedback · POST /feedback/with-screenshotContact + in-app feedback

Seed lookups used when creating/estimating stories. Stable IDs.

MethodPathDescription
GET/story_typesfeature, bug, chore, release (+ allow_points)
GET/story_statesunstarted … accepted, rejected
GET/effort_scalesavailable estimate scales
GET/effort_scales/{scale_id}/valuesthe point values in a scale
MethodPathDescription
GET/projectsList your projects
POST/projectsCreate 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-logProject activity stream (owner)
GET/projects/{id}/eventsCursor-paginated event stream (viewer) — see Events
MethodPathDescription
GET/projects/{id}/membershipsList members (viewer)
POST/projects/{id}/membershipsInvite 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_keysList / mint agent keys (owner)
DELETE/projects/{id}/agent_keys/{kid}Revoke an agent key (owner)

All story writes need the member role.

MethodPathDescription
GET/projects/{id}/storiesList stories (paginated, filterable) (viewer)
POST/projects/{id}/storiesCreate 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}/transitionsChange state with validation
POST/projects/{id}/stories/bulk_transitionTransition many stories (1–100) at once
POST/projects/{id}/stories/bulk-deleteDelete many stories
POST/projects/{id}/stories/bulk-duplicateDuplicate many stories
POST/projects/{id}/stories/{sid}/duplicateDuplicate 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 } ] }.

All member. List/GET on most is (viewer).

MethodPathBody / 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)

member for writes, (viewer) for reads.

MethodPathDescription
GET / POST/projects/{id}/labelsList / create a label
PUT / DELETE/projects/{id}/labels/{lid}Update / delete a label
POST/projects/{id}/labels/{lid}/archiveArchive (soft-hide) a label
MethodPathDescription
GET/projects/{id}/iterationsList iterations (member)
POST/projects/{id}/iterationsCreate a manual iteration (owner)
DELETE/projects/{id}/iterations/{itid}Delete an iteration (owner)
MethodPathDescription
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}/preferencesYour board preferences for this project (member)
MethodPathDescription
GET/projects/{id}/eventsCursor-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.

MethodPathDescription
POST/projects/{id}/import (+ /json)Multipart upload. source=pivotal, jira, asana, gitlab, shortcut, trello, linear, plane, plane_json.
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.

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.

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.

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,owners

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"] } }
StatuscodeWhen
400invalid_parameterbad input; message in error, no details (most validation: blank/length/null-byte/email)
400validation_failedstructured input error; details.fields is an array of offending field names
401unauthenticatedmissing/invalid token
403unauthorized_operationauthenticated but insufficient role
404unfound_resourcenot found — also returned to non-members
409conflictresource conflict (e.g. duplicate)
409idempotency_conflictIdempotency-Key reused with a different body
422invalid_transitionillegal state move; details carries { from, to, allowed }
500internal_errorserver 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"] } }

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).