Skip to content

API Guide

The East Agile Tracker API is designed for agents as much as for humans. Everything you can do in the UI, you can do over the API — and a few things the UI doesn’t expose are there too.

This guide gets you from zero to “scripting your backlog” in under ten minutes. For the full endpoint reference, see API Specification.

You authenticate with a key in the X-TrackerToken header. There are two kinds of key you mint yourself, and a third that an MCP client obtains for you:

  • User keys (ea_user_…) — Act as you. Create them in Account Settings → API Keys. Use these for personal scripts, CLI tools, integrations.
  • Agent keys (ea_agent_…) — Act as a named agent in one project. Create them in Project Settings → Agents. Use these for AI agents — Claude Code, Codex, your own — that should participate in the project as named teammates.
  • MCP tokens (ea_mcp_…) — OAuth 2.1 access tokens issued to an MCP client (Claude, an IDE) after you approve it on the consent page. They act as you, and you can revoke them under Account Settings → Connected apps.

The one-time dialog after creating a personal API key in Account Settings, with the key masked in this capture

The Agent tab's create-key form with a name and the member role selected, below the setup instructions

The differences between the two you mint:

User keyAgent key
ScopeAll your projectsOne specific project
Identity in audit logYour nameThe agent’s name
RoleYour role in each projectSet at key creation (viewer, member, or manager — never above the minting member’s own role)
RevocationRevoke a key; you keep access via other keys/sessionsRevoke or rotate a key; the agent loses access immediately
Best forPersonal automation, scriptsAI agents that should be distinguishable from you in the history

Authorization: Bearer … also works if you prefer that header style.

Get your projects:

Terminal window
curl https://eastagiletracker.com/api/v1/projects \
-H "X-TrackerToken: $TRACKER_TOKEN"

Or for an agent key, list the project it’s scoped to:

Terminal window
curl https://eastagiletracker.com/api/v1/projects \
-H "X-TrackerToken: ea_agent_xxxxx"

The API is JSON, REST-ish, versioned at /api/v1/. Same shapes for humans and agents.

Terminal window
curl -X POST https://eastagiletracker.com/api/v1/projects \
-H "X-TrackerToken: $TRACKER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Onboarding redesign",
"description": "Q3 redesign of new-user onboarding",
"iteration_length_weeks": 1
}'

The response includes the project_id and any defaults the server applied (estimate scale, done state, etc.).

Terminal window
curl -X POST https://eastagiletracker.com/api/v1/projects/$PROJECT_ID/stories \
-H "X-TrackerToken: $TRACKER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Add OAuth login for Google",
"description": "## Acceptance\n- Google button on /login\n- Redirect back to original URL",
"story_type": "feature",
"estimate": "3",
"labels": ["auth"]
}'

estimate is the scale value’s label as a string — "3", or "13" on the Fibonacci scale — because it has to match a point on the project’s scale. A JSON number is rejected.

The transition endpoint validates the requested move and returns the allowed next states on error:

Terminal window
curl -X POST https://eastagiletracker.com/api/v1/projects/$PROJECT_ID/stories/$STORY_ID/transitions \
-H "X-TrackerToken: $TRACKER_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "to": "started" }'

The field is to (not to_state). If the move is illegal — say you tried to skip from unstarted straight to accepted — the response is 422 invalid_transition with structured error details:

{
"code": "invalid_transition",
"error": "Cannot move story from `unstarted` to `accepted`",
"details": { "from": "unstarted", "to": "accepted", "allowed": ["started"] }
}

This is one of the small things that makes the API agent-friendly: an agent can read details.allowed and choose the right next move without scraping prose.

rejected is terminal for the transition endpoint. To put a rejected story back to work, POST …/stories/{sid}/restart; POST …/stories/{sid}/reject is the verb form of rejecting a delivered one.

Terminal window
curl -X POST https://eastagiletracker.com/api/v1/projects/$PROJECT_ID/stories/$STORY_ID/comments \
-H "X-TrackerToken: $TRACKER_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "text": "Investigation done. Picking this up." }'

The comment is attributed to whoever owns the API key — if it’s an agent key, the comment’s author is the agent.

Every write endpoint accepts an Idempotency-Key header. Retry the same key with the same body, get the same response back. Retry the same key with a different body, get a 409 idempotency_conflict:

Terminal window
curl -X POST https://eastagiletracker.com/api/v1/projects/$PROJECT_ID/stories \
-H "X-TrackerToken: $TRACKER_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "name": "Refactor auth middleware", "story_type": "chore" }'

This is critical for agents in retry loops — crash mid-write, retry with the same key, no duplicate stories.

Move many stories at once. Each story is judged independently; one illegal move doesn’t fail the others.

Terminal window
curl -X POST https://eastagiletracker.com/api/v1/projects/$PROJECT_ID/stories/bulk_transition \
-H "X-TrackerToken: $TRACKER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"story_ids": [101, 102, 103],
"to": "delivered"
}'

For agents that want to react to what humans do, poll the events endpoint:

Terminal window
curl "https://eastagiletracker.com/api/v1/projects/$PROJECT_ID/events?since=$LAST_CURSOR&types=story.created,story.transitioned,comment.added" \
-H "X-TrackerToken: $TRACKER_TOKEN"

Response is a cursor-paginated stream of events with the actor, the resource, and the change. Each event has an ID; pass the last ID you saw as since to resume where you left off. No webhooks, no scraping, no missed events. The stream needs the member role — a viewer gets 403.

GET /projects/{id}/search?q=<query> runs a powerful full-text + structured search over the project’s stories. The query language is modelled on GitHub’s issue-search qualifiers — so syntax you (or an AI agent) already know from GitHub mostly carries over.

Terminal window
curl -G "https://eastagiletracker.com/api/v1/projects/$PROJECT_ID/search" \
-H "X-TrackerToken: $TRACKER_TOKEN" \
--data-urlencode 'q=payment crash type:bug,chore owner:@me created:>2026-05-01'

The response is a JSON envelope, stories ranked by relevance:

{ "results": [ /* canonical story objects */ ], "total": 42, "limit": 50, "offset": 0 }

total is the full match count, not the page size. Page with limit (default 50, max 1000) and offset; order with sort=relevance (default), created, created_asc, updated, or state.

  • Free text matches a story’s title, reference, and description (full-text, stemmed and ranked). Wrap an exact phrase in "quotes".
  • Qualifiers are field:value. Comma-separate alternatives (OR within a field): type:bug,chore. Space-separate qualifiers (AND across them).
  • Negate any term or qualifier with a leading -: -label:wontfix.
  • Ranges for dates and points: inclusive a..b, or open-ended >x / <x.
QualifierExampleMatches
type:type:bug,chorestory type(s)
state:state:started,finishedworkflow state(s)
label:label:"my label"a label
epic:epic:"Checkout"stories in an epic
priority:priority:p1priority
points:points:3 · points:1..5 · points:>3estimate value or range
iteration:iteration:42iteration id
created: updated: started: completed: release:created:2026-05-01..2026-06-01 · updated:>2026-06-01a date or range (day-granular); release: is the story’s release date
owner: requester: follower: reviewer: commenter: mention:owner:claire · owner:@mea person by name or email — members and agents, mention: included; @me is you
has:blockerhas:blockerhas an open blocker
is:is:unestimated · is:icebox · is:backlog · is:blockeda flag

mywork: is an alias for owner:mywork:me is owner:@me. The older scheduled: qualifier is retired and silently ignored; use release:.

Comma-OR (type:bug,chore) applies to the facet qualifiers; the people qualifiers (owner: requester: follower: reviewer: commenter: mention:) take a single value.

payment crash full text "payment" AND "crash"
"exact phrase" a phrase
type:bug,chore state:started bugs or chores that are started
owner:@me -label:wontfix mine, excluding the wontfix label
points:3..8 created:2026-05-01..2026-06-01 estimated 3-8, created in May
follower:tomas has:blocker tomas follows it and it's blocked
is:backlog updated:>2026-06-01 backlog items touched since Jun 1

The same query string drives the board’s search box (which opens a live results column) and this API — one grammar for humans and agents alike. Searching the content of comments, tasks, and blockers is on the roadmap; today free text covers the story’s own title, reference, and description.

The live OpenAPI 3 spec is at:

https://api.eastagiletracker.com/api/v1/openapi.json

Swagger UI is at:

https://api.eastagiletracker.com/api/v1/docs/

/openapi.json and /docs are unauthenticated — an agent can read the contract before it has a key. Once it holds a key, /api/v1/meta (which requires a valid key) returns its identity and the per-story-type transition graph; the reference-data lookups (/story_types, /story_states, /effort_scales, /priority_scales) are also unauthenticated. Together they let agents answer “what can I do here?” without trial-and-error 403s.

The served openapi.json carries request-body schemas for the write endpoints, including each field’s maxLength, so a client can validate before it sends. The Specification summarises the same shapes.

For interactive automation — driving a logged-in browser session from a script, or remote-controlling the UI for tutorials — there’s a WebSocket channel:

const ws = new WebSocket('wss://eastagiletracker.com/ws/control?token=SESSION_JWT')
ws.send(JSON.stringify({ action: 'get_state', id: 'req-1' }))

The token is the browser session’s JWT, not an API key — an ea_user_* or ea_agent_* key is refused before the upgrade. Most users never need this; it’s there for the cases where REST isn’t enough.

If you’re scripting a bulk migration:

Terminal window
curl -X POST https://eastagiletracker.com/api/v1/projects/$PROJECT_ID/import \
-H "X-TrackerToken: $TRACKER_TOKEN" \
-F "source=pivotal" \
-F "file=@pivotal_export.csv"

Supported file sources: pivotal, jira, asana, gitlab, shortcut, trello, linear, plane, plane_json, eat (East Agile Tracker’s own export — the round-trip format). The multipart endpoint runs synchronously and answers with the result counts.

GitHub imports from the API instead of a file, via the JSON endpoint — no file, just the repository coordinates:

Terminal window
curl -X POST https://eastagiletracker.com/api/v1/projects/$PROJECT_ID/import/json \
-H "X-TrackerToken: $TRACKER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"source": "github",
"owner": "octocat",
"repo": "hello-world",
"token": "ghp_…",
"include_pull_requests": false,
"include_milestones": false,
"include_releases": false,
"include_dependencies": false
}'

The JSON endpoint is asynchronous: it answers 202 with { "import_id", "status" } and you poll GET /projects/{id}/imports/{import_id} until the job reaches done or failed. Only one import runs per project at a time — a second call while one is in flight is 409 import_already_running. The whole loop, with the job’s progress fields, is in Populate a project from a GitHub repo.

The token is optional on the wire, but the fetch itself always authenticates — it runs on GitHub’s GraphQL API, which has no unauthenticated tier. Omit token and the server substitutes its platform token: public repositories only, shared by every caller, and refused with import_github_shared_quota_low when its GraphQL budget drops below 500 points. A private repository, or a deployment that configured no platform token (import_github_no_token), needs yours. Whichever token runs, it is used only for the upstream GitHub calls and is never stored or echoed back. Full detail, including GitHub’s 60-request unauthenticated REST ceiling, is in Populate a project from a GitHub repo.

Dry-run preview. Add "dry_run": true (JSON) or -F "dry_run=true" (multipart) to any source. The import parses, resolves, and de-duplicates exactly as a real run, produces the same result counts (imported, skipped, errors, unmatched), then rolls everything back — nothing is written. On the JSON endpoint the counts arrive on the polled job, dry run or not.

Limits. An upload body is capped at 10 MiB, and a single import at 5,000 stories; exceeding either is a 400 with nothing written. Re-importing a file is safe — rows already imported (matched by source id) are skipped, not duplicated.

Any project role can list the formats; downloading one is owner-only:

Terminal window
# The registered export formats: { id, name, content_type, drops, includes_archived }
curl https://eastagiletracker.com/api/v1/projects/$PROJECT_ID/export/formats \
-H "X-TrackerToken: $TRACKER_TOKEN"
# Download one format (eat is the full-fidelity round-trip CSV)
curl https://eastagiletracker.com/api/v1/projects/$PROJECT_ID/export/eat \
-H "X-TrackerToken: $TRACKER_TOKEN" -o project-export.csv

Interchange format ids: eat, jira, pivotal, shortcut, trello, asana, gitlab, linear, plane, plane_json, plus the document formats pdf and docx. Every attachment is downloadable as one zip from GET /projects/{id}/export/attachments.

All errors are JSON with at minimum:

{
"code": "invalid_transition",
"error": "Cannot move story from `unstarted` to `accepted`"
}

Many error responses also include a details object — details.fields (an array of offending field names) on validation_failed, and details.allowed (alongside from/to) on 422 invalid_transition. Use them. A 429 rate_limited carries a Retry-After header in the same JSON envelope.

List endpoints accept limit and cursor. Cursor is opaque; pass the next_cursor from the previous response. The limit cap is per endpoint — 200 on stories, comments, and projects, 500 on events, 1000 on search and the audit log. A plain (non-cursor) list that had to truncate its response says so in headers: X-Tracker-Pagination-Truncated, -Limit, -Offset, and -Next-Offset, which you pass back as offset= for the next page. There is no total-count header.