REST API reference.
Read and write tasks, subtasks, comments, projects and more over plain HTTPS, with an organization API key. Prefer MCP for AI clients; use REST for scripts and integrations.
Base URL and versioning
https://app.agent-task.com/api/v1This is the one and only surface; there is no other alias. It is version v1: new endpoints, optional fields and optional query parameters ship within v1, and a breaking change would need a new major version rather than a silent change. Paths are plural, kebab-case nouns (/task-groups), identifiers are UUIDs, and JSON fields are camelCase.
Authentication
Every endpoint takes an organization API key as a bearer token. Create a key in the app under Settings → API keys. Admins can always create keys; other members can once an admin enables self-service keys (Admin → Connectors → API) or grants them the “Manage API keys” permission. The key is shown once. The organization comes from the key, never from the request, and a key can only reach resources in its own organization.
Authorization: Bearer amk_YOUR_KEYKeys carry scopes in the form resource:read or resource:write, and each route requires one. New keys get tasks, subtasks, comments and feature_requests read and write by default; everything else (spaces, projects, task groups, crews and more) is opted in per key. To discover the spaceUuid that most endpoints need, give the key spaces:read and call GET /spaces. GET /me needs only a valid key and returns its organization, scopes and current rate-limit budget. A key can also be confined to specific spaces.
OAuth sign-in is for the MCP endpoint only; the REST API accepts organization API keys, not OAuth tokens. Call it from a server or script, never from browser code: a key shipped in a page is visible to every visitor.
Endpoints
The generated spec is the complete, always-current list; this is the map. {id} is a task UUID.
| Resource | Paths under /api/v1 |
|---|---|
| Introspection | GET /me |
| Spaces, users | GET /spaces, GET /spaces/{spaceUuid}, GET /users |
| Tasks | GET, POST /tasks · GET, PATCH, DELETE /tasks/{id} |
| Subtasks | GET, POST /tasks/{id}/subtasks · PATCH, DELETE /tasks/{id}/subtasks/{subtaskUuid} |
| Comments | GET, POST /tasks/{id}/comments · PATCH, DELETE /tasks/{id}/comments/{commentUuid} |
| Dependencies, assignments | /tasks/{id}/dependencies and /tasks/{id}/assignments |
| Task groups | GET, POST /task-groups · PATCH, DELETE /task-groups/{groupUuid} |
| Projects | GET, POST /projects · GET, PATCH /projects/{projectUuid} · POST …/archive, …/conclude |
| Crews | GET, POST /crews · GET, PATCH, DELETE /crews/{crewUuid} |
| Feature requests | GET, POST /feature-requests · GET, PATCH, DELETE /feature-requests/{id} |
Pagination
List endpoints take limit (1 to 500, default 100) and offset (default 0) and return an envelope. hasMore is true while offset + data.length < total. Advance by data.length, not by the limit you asked for: the task list serves at most 200 rows per page even when you request more, and echoes the limit it actually used.
{
"data": [ { "...": "resource" } ],
"pagination": { "limit": 100, "offset": 0, "total": 42, "hasMore": false }
}Errors
Errors are JSON with an HTTP status. Authentication, scope, billing and rate-limit failures use { error, code?, required? }:
// 401 — missing or unknown key
{ "error": "Invalid API key" }
// 403 — valid key, missing scope
{ "error": "Insufficient scope", "required": "tasks:write" }
// 403 — organization has no active trial or subscription
{ "error": "An active trial or subscription is required for API access.", "code": "billing_required" }
// 429 — per-key rate limit (with a Retry-After header)
{ "error": "Rate limit exceeded for this API key", "code": "rate_limited" }Parameter-validation failures and not-found responses come from the web framework and use a different shape, so read message as well as error when a response has no code. A request for another organization’s resource is a plain 404, not a 403. Responses carry no WWW-Authenticate header.
Only Authorization: Bearer amk_… reaches this API. A request with no token, or with a token that is not an amk_ key (an MCP OAuth access token, for example), is handled by the web app instead, so the usual symptom is a 400 or 405 rather than a 401. If you see one, check the header first.
// 400 (bad or missing parameter) and 404 (unknown resource or route)
{ "statusCode": 404, "error": "Not Found", "message": "Space not found" }Rate limits
Each key gets 600 requests per 60 seconds by default, shared with the same key’s MCP traffic. Every response past authentication carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (epoch seconds). Over the limit you get a 429 with Retry-After: 60 and code: rate_limited; wait, then retry. These are per-minute burst limits; the daily request allowances listed on the pricing page are not what this limiter enforces.
Idempotency
The REST endpoints do not take an idempotency key, so retrying a POST that timed out can create a duplicate. List first, or make the write safe to repeat (for example, search for the title before creating). GET, PATCH and DELETE on a known UUID are safe to retry. The MCP tools create_task, create_subtask and add_comment do accept an idempotencyKey; see the tool catalog.
Testing your integration
There is no separate test mode or sandbox host: every call hits your real workspace, and every write is attributed to the key in the audit trail. To experiment safely, use a throwaway workspace or a dedicated space, mint a key confined to that space with only the scopes you need, and revoke the key when you are done.
Examples
Set AGENT_TASK_API_KEY first. Replace SPACE_UUID and TASK_UUID with ids from the previous call.
# 1. Find your space (needs the spaces:read scope on the key)
curl -s "https://app.agent-task.com/api/v1/spaces" \
-H "Authorization: Bearer $AGENT_TASK_API_KEY"
# 2. List tasks in it (spaceUuid is required)
curl -s "https://app.agent-task.com/api/v1/tasks?spaceUuid=SPACE_UUID&status=todo&limit=20" \
-H "Authorization: Bearer $AGENT_TASK_API_KEY"
# 3. Create a task (spaceUuid and title are required)
curl -s -X POST "https://app.agent-task.com/api/v1/tasks" \
-H "Authorization: Bearer $AGENT_TASK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"spaceUuid":"SPACE_UUID","title":"Write the launch checklist","priority":"high"}'
# 4. Comment on it (content is required)
curl -s -X POST "https://app.agent-task.com/api/v1/tasks/TASK_UUID/comments" \
-H "Authorization: Bearer $AGENT_TASK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content":"Draft is up for review."}'const BASE = "https://app.agent-task.com/api/v1";
const headers = {
Authorization: `Bearer ${process.env.AGENT_TASK_API_KEY}`,
"Content-Type": "application/json",
};
async function api(path, init) {
const res = await fetch(`${BASE}${path}`, { ...init, headers });
if (res.status === 429) {
// Per-key limit: wait the advertised time, then retry.
const wait = Number(res.headers.get("Retry-After") ?? 60);
await new Promise((r) => setTimeout(r, wait * 1000));
return api(path, init);
}
if (!res.ok) throw new Error(`${res.status} ${JSON.stringify(await res.json())}`);
return res.json();
}
// Walk every page of a list (limit/offset pagination).
async function* allTasks(spaceUuid) {
for (let offset = 0; ; ) {
const page = await api(`/tasks?spaceUuid=${spaceUuid}&limit=100&offset=${offset}`);
yield* page.data;
if (!page.pagination.hasMore) return;
offset += page.data.length;
}
}
const task = await api("/tasks", {
method: "POST",
body: JSON.stringify({ spaceUuid: "SPACE_UUID", title: "Write the launch checklist" }),
});import os
import time
import requests
BASE = "https://app.agent-task.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['AGENT_TASK_API_KEY']}"
def api(method, path, **kwargs):
res = session.request(method, BASE + path, **kwargs)
if res.status_code == 429:
time.sleep(int(res.headers.get("Retry-After", 60)))
return api(method, path, **kwargs)
res.raise_for_status()
return res.json()
# Walk every page of a list (limit/offset pagination).
def all_tasks(space_uuid):
offset = 0
while True:
page = api("GET", "/tasks", params={"spaceUuid": space_uuid, "limit": 100, "offset": offset})
yield from page["data"]
if not page["pagination"]["hasMore"]:
return
offset += len(page["data"])
task = api("POST", "/tasks", json={"spaceUuid": "SPACE_UUID", "title": "Write the launch checklist"})OpenAPI and Swagger
The spec is generated from the running routes, so it is the authority for every path, parameter and required field.
To use a client tool such as Postman or Insomnia, import the openapi.json URL below.
- Swagger UI https://app.agent-task.com/api/v1/documentation
- openapi.json https://app.agent-task.com/api/v1/openapi.json
- Connect an AI client over MCP