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

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.

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:

LevelWho passesTypical operations
public vieweranyone, on a project whose visibility is publicreads of the board: stories, iterations, search, story and epic activity (with actor details redacted)
viewerviewer, member, managerreads (list/get stories, search, metrics, export format list)
membermember, managerall work-item writes (stories, tasks, comments, …), the event stream
managermanager onlyproject 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.

MethodPathDescription
GET/openapi.jsonThe live OpenAPI 3 spec, request bodies included. 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.
GET/api/health · /api/configLiveness, and the deployment’s public configuration (single-org mode, which optional features are on, the instance name). Unauthenticated, outside /v1.

Session endpoints, unauthenticated unless noted. The SPA drives these; scripts normally use an API key instead.

MethodPathDescription
POST/auth/registerRegister a new account — reCAPTCHA-guarded; the account then passes the SMS challenge
POST/auth/sms/challenge · /auth/sms/status · /auth/sms/bypassSend / check the sign-up SMS code (bypass is operator-gated)
GET/auth/configWhich sign-in methods the deployment offers
POST/auth/loginSign in with email + password; returns a session JWT, or a TOTP challenge
POST/auth/login/totpFinish a sign-in with an authenticator code or a recovery code
POST/auth/passkey/login/start · /auth/passkey/login/finishPasswordless WebAuthn sign-in
GET/auth/oauth/github/start · /auth/oauth/github/callback · /auth/oauth/google/start · /auth/oauth/google/callback · POST /auth/oauth/exchangeOAuth sign-in with GitHub or Google
POST/auth/refresh · /auth/refresh/revokeRotate the refresh token / revoke it
POST/auth/logoutSign out (revokes the refresh token)
POST/auth/forgot-password · /auth/reset-passwordRequest a reset email / use the reset token
POST/auth/accept-invite/lookup · /auth/accept-inviteResolve an invitation token → email / accept the project invitation (after authenticating)

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

MethodPathDescription
GET/meCurrent user profile
PUT/meUpdate profile
DELETE/meDelete account — refused while you are the sole owner of an org or of a project with other members
GET/me/deletion-impactWhat deleting the account would remove and what blocks it
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/totp · POST /me/totp/setup · /me/totp/verify · /me/totp/disableTwo-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/activityYour activity across all projects
GET/me/storiesStories 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}/ackThe @-mention inbox (unacked=true to filter) and acknowledgement — also folded into the notification feed below
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
GET / PUT/agent/meAn 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-screenshotContact + in-app feedback. Outside /v1; rate-limited per IP

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
GET/priority_scales · /priority_scales/{scale_id}/valuesthe priority scales and their values (priority_id on a story resolves here)

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.

MethodPathDescription
GET / POST/organizationsList 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-removeChange 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-ownershipHand the owner role to another member
PUT/organizations/{oid}/memberships/{member_id}/anonymizationMask a member’s name / email / avatar org-wide
GET/organization-invitations/{token} · POST …/{token}/acceptResolve / accept an emailed org invitation
POST/organizations/{oid}/export · GET / POST …/export/jobs · GET …/export/jobs/{job_id} · …/export/jobs/{job_id}/downloadOwner-only org export: a zip with a SQL dump and every attachment, run as a job
MethodPathDescription
GET/projectsList your projects (limit ≤ 200)
POST/projectsCreate 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}/pinPin / unpin the project on your project list
POST/projects/{id}/transfer-organizationMove the project to another organization (manager)
POST/projects/{id}/slack/testSend a test message to the project’s Slack feed (manager)
GET / POST/projects/{id}/showcase_claim_eligibility · /projects/{id}/showcase_claim · /projects/{id}/showcase_seedPublic showcase projects: check whether you can claim one, claim it, seed it
GET/projects/{id}/audit-logAudit-log read — project history plus per-story / per-epic activity via surface=; access varies by surface, see below
GET/projects/{id}/eventsCursor-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.

MethodPathDescription
GET/projects/{id}/membershipsList members (viewer)
POST/projects/{id}/membershipsInvite 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-existingOrg members not yet on the project / add one without an email invite (manager)
POST/projects/{id}/members/joinAn 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}/anonymizationMask a member’s name / email / avatar on this project (manager)
GET / POST/projects/{id}/agent_keysList / 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/onboardingThe 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}/avatarRotate an agent’s key (identity and history kept) / upload its avatar

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/{sid}/reject · /projects/{id}/stories/{sid}/restartReject 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}/unarchiveArchive / unarchive one story
POST/projects/{id}/stories/bulk_transitionTransition many stories (1–100) at once
POST/projects/{id}/stories/bulk-archive · bulk-delete · bulk-duplicate · bulk-moveArchive, delete, duplicate, or move (to a panel / position) many stories
POST/projects/{id}/stories/{sid}/duplicateDuplicate one story
GET / POST / DELETE/projects/{id}/stories/{sid}/epics · …/epics/{eid}The story’s epic membership
GET/short-links/{code} · /story-referencesResolve 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 } ] }.

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

MethodPathDescription
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-attachmentsAttachments, same caps as stories
GET/projects/{id}/analytics/epics · …/analytics/epics/{eid}Per-epic progress: burnup, throughput, health, forecast (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

Reads are open to any project role, and anonymous on a public project.

MethodPathDescription
GET/projects/{id}/iterationsList 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-previewThe dates the first iteration would get, shown in the seeding confirmation
POST/projects/{id}/iterationsCreate a manual iteration (member)
DELETE/projects/{id}/iterations/{itid}Delete an iteration (manager)
PUT/projects/{id}/iterations/{itid}/velocityOverride one iteration’s velocity without changing the project strategy (manager)
GET/projects/{id}/iterations/{itid}/done-storiesThe accepted stories of a closed iteration, paginated
MethodPathDescription
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/groupingThe Backlog’s projected iteration groups (viewer)
GET / PUT/projects/{id}/preferencesYour board preferences for this project — any project role, your own row only
MethodPathDescription
GET/projects/{id}/eventsCursor-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.

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.

MethodPathDescription
GET/me/notificationsYour notification feed. Filters: unread=true, since_id=, kind= (mentions / reviews / stories / invitations); paginate with cursor= / limit=
GET/me/notifications/unread-countUnread totals — { unread_count, by_category: { mentions, reviews, stories, invitations } }
POST/me/notifications/read-allMark everything read; returns the fresh counts
POST/me/notifications/{id}/ackMark one item read (idempotent)
POST/me/notifications/{id}/acceptAccept a project / organization invitation from the feed (member tokens only)
POST/me/notifications/{id}/declineDecline 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/streamLive 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.

MethodPathDescription
POST/projects/{id}/importFile 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/jsonJSON 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.

MethodPathDescription
GET/projects/{id}/export/formatsRegistered 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/attachmentsEvery 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.

MethodPathDescription
GET / POST/projects/{id}/backups · GET …/backups/{snapshot_id} · …/backups/healthList 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).

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.

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.

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.

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.

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
409stale_write · import_already_runningthe story changed since your expected_updated_at · an import is already in flight
412precondition_failedIf-Match didn’t match the resource’s current ETag; details carries expected and current
413request_too_largethe body exceeds the route’s size limit
422invalid_transitionillegal state move; details carries { from, to, allowed }
429rate_limitedtoo many requests from this IP on a rate-limited route; Retry-After header
500internal_errorserver fault — generic message; safe to retry
503not_configuredthe 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"] } }

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