Developer
REST API
Atlas exposes a hand-curated, hardened public REST surface under /v1/. One bearer header gets you in. This guide covers auth, success and error responses (every status code with an example body), rate limits, idempotency, pagination, and webhooks, and points you at the interactive reference for every route.
Start at the interactive reference
Every endpoint, parameter, scope, request body, and status code, rendered live from the OpenAPI spec with copyable request samples and a Try It console. This guide explains the concepts; the reference is the source of truth for each route.
Open the API reference
Rendered live from https://api-atlas.wrxstack.com/v1/openapi.json.
Quickstart
Go from nothing to your first authenticated request in about a minute.
Mint a Personal Access Token
Open Settings, then API access, and click New token. Pick the narrowest scope set (start withtasks:read). The token is shown once and starts withatlas_pat_.Send your first request
Pass the token as a bearer header and read your tasks. This lists the first 50 tasks in your workspace.curl -H "Authorization: Bearer atlas_pat_REPLACE_ME" \ https://api-atlas.wrxstack.com/v1/tasks?limit=50Go deep in the reference
Open the interactive reference to see every route, its parameters, and its full response shape, each with a request sample you can copy.
Keep tokens narrow
Authentication
Every /v1 request carries a bearer token. The same header accepts a Personal Access Token (atlas_pat_...) or a session JWT. PATs are the recommended path for server-to-server use because they are scope-bounded and revocable independently of any user session.
Authorization: Bearer atlas_pat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxFull authentication guide
Scopes are enforced per request
403 (insufficient_scope) if the token is missing the scope that route requires. The problem body lists requiredScopes and grantedScopes so you know exactly what to add. Session-JWT calls bypass the scope gate (the session is implicitly all-scopes). See the 403 example below.Success responses
Successful calls return application/json (never problem+json). Reads answer 200; creates answer 201 with a Location header. List endpoints are always an { items, nextCursor } envelope, so a single item and a thousand share one shape. Every response also carries an x-request-id header for support tracing.
OK - GET /v1/tasks (paginated list)
Collection reads return the requested page under items plus an opaque nextCursor (null on the last page). Rate-limit headers ride along on every 2xx.
HTTP/1.1 200 OK
Content-Type: application/json
x-request-id: 3f1a9c02-7d4e-4b1a-9c8f-2a6b1e0d5f77
X-RateLimit-Class: read
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 299
{
"items": [
{
"id": "tsk_01HW3T2K8X9Y2VPRZGQX9NDY1F",
"projectId": "prj_01HW3S9QZ4M0V6B7C8D9E0F1G2",
"title": "Review Q3 forecast",
"status": "TODO",
"priority": "HIGH",
"dueOn": "2026-05-01T17:00:00Z",
"assigneeId": "usr_01HW3RA2B3C4D5E6F7G8H9J0K1",
"version": 1,
"createdAt": "2026-04-18T09:12:44Z",
"updatedAt": "2026-04-18T09:12:44Z"
}
],
"nextCursor": "eyJpZCI6InRza18wMUhXM1QifQ=="
}Created - POST /v1/tasks (the created resource)
Creates return the full resource, including its server-assigned id, version, and timestamps, plus a Location header. Replaying the same Idempotency-Key returns this exact 201 again (see Idempotency).
HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/tasks/tsk_01HW3T2K8X9Y2VPRZGQX9NDY1F
x-request-id: 9b2c4d6e-1f38-4a5b-8c7d-0e9f1a2b3c4d
{
"id": "tsk_01HW3T2K8X9Y2VPRZGQX9NDY1F",
"projectId": "prj_01HW3S9QZ4M0V6B7C8D9E0F1G2",
"title": "Review Q3 forecast",
"status": "TODO",
"priority": "HIGH",
"dueOn": "2026-05-01T17:00:00Z",
"assigneeId": null,
"version": 1,
"createdAt": "2026-04-18T09:12:44Z",
"updatedAt": "2026-04-18T09:12:44Z"
}Errors
Every non-2xx response uses one consistent envelope: RFC 9457 / RFC 7807 problem+json. Always branch on Content-Type - success is application/json, any error is application/problem+json - and read status from the body, not just the status line.
The error envelope
Five members are always present. The type URI is the stable, machine-readable identifier for the problem class (branch on it, not on the human copy); detail is the human message for this specific request. Generic problems use type: "about:blank" and lean on title. Specific problem types add extension members, called out per status below.
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
x-request-id: f47ac10b-58cc-4372-a567-0e02b2c3d479
{
"type": "https://atlas.dev/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This Personal Access Token is missing one of the required scopes: tasks:write.",
"requiredScopes": ["tasks:write"],
"grantedScopes": ["tasks:read"],
"requestId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}| Field | Type | Meaning |
|---|---|---|
type | string (uri) | Stable identifier for the problem class. https://atlas.dev/errors/<slug> for known types, else about:blank. |
title | string | Short, human summary of the problem type. Same for every instance of that type. |
status | integer | The HTTP status, mirrored in the body so it survives logging and proxies. |
detail | string | Human explanation specific to this request. Safe to surface to developers. |
requestId | string | Correlation id, also returned in the x-request-id header. Quote it in support requests. |
Extension members you will meet below: errors[] (422 validation, with a path, message, and code per field), requiredScopes / grantedScopes (403 insufficient scope), and rateLimit (429).
At a glance
| Status | When you see it | Retry? |
|---|---|---|
| 400 | Malformed request or a value a storage constraint rejects. | No, fix the request |
| 401 | Missing, unknown, revoked, or expired token. | No, re-auth |
| 403 | Token lacks the required scope, or origin is blocked. | No, widen the token |
| 404 | Resource missing, or invisible to your tenant. | No |
| 409 | Unique collision or stale If-Match version. | Yes, re-read then retry |
| 422 | Body parsed but failed field validation. | No, fix the fields |
| 429 | Rate bucket exhausted. Retry-After header set. | Yes, after Retry-After |
| 500 | Server fault. Body is generic; requestId traces it. | Yes, with backoff |
Every status, with an example
Bad Request
The request itself is malformed - unparseable JSON, a value that a storage constraint rejects (for example a string that is too long), or a required value missing at the persistence layer. Field-level schema failures come back as 422 instead.
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
x-request-id: 1a2b3c4d-5e6f-4708-8192-a3b4c5d6e7f8
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "One or more values are invalid.",
"requestId": "1a2b3c4d-5e6f-4708-8192-a3b4c5d6e7f8"
}Unauthorized
No bearer token, or the token is empty, unknown, revoked, or expired. Mint or rotate the PAT and retry. The detail narrows the cause (for example Personal access token revoked).
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
x-request-id: 7c9e2f14-3a5b-4d6e-8f70-1b2c3d4e5f60
{
"type": "about:blank",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid or expired access token",
"requestId": "7c9e2f14-3a5b-4d6e-8f70-1b2c3d4e5f60"
}Forbidden
The token is valid but not allowed to do this. The common case is a PAT missing a required scope; the body then carries requiredScopes and grantedScopes so you can widen the token. A browser origin outside the CORS allowlist also 403s.
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
x-request-id: f47ac10b-58cc-4372-a567-0e02b2c3d479
{
"type": "https://atlas.dev/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This Personal Access Token is missing one of the required scopes: tasks:write.",
"requiredScopes": ["tasks:write"],
"grantedScopes": ["tasks:read"],
"requestId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}Not Found
The resource does not exist, or it exists in another tenant your token cannot see. Atlas does not distinguish the two on purpose - a cross-tenant id is indistinguishable from a missing one, so ids never leak across tenants.
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
x-request-id: 2d4f6a8c-0e13-4257-9b8d-6f0a1c3e5d70
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"requestId": "2d4f6a8c-0e13-4257-9b8d-6f0a1c3e5d70"
}Conflict
The write collides with existing state: a unique value already in use, an If-Match version that is now stale (optimistic concurrency), or a transient write conflict. Re-read the resource, merge, and retry with the fresh version.
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
x-request-id: 8e0c2a46-9f7b-4d31-a0c5-2e4f6a8b0d13
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"detail": "The operation conflicted with a concurrent change. Please retry.",
"requestId": "8e0c2a46-9f7b-4d31-a0c5-2e4f6a8b0d13"
}Unprocessable Entity
The body parsed but failed schema validation. The response adds an errors array with one entry per failing field: its path, a human message, and a machine code. Fix the listed fields and resend.
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
x-request-id: 5b7d9f11-2c4e-4638-8a0b-1d3f5a7c9e02
{
"type": "https://atlas.dev/errors/validation",
"title": "Validation failed",
"status": 422,
"detail": "One or more fields failed validation",
"errors": [
{
"path": "title",
"message": "String must contain at least 1 character(s)",
"code": "too_small"
},
{
"path": "priority",
"message": "Invalid enum value. Expected 'LOW' | 'MEDIUM' | 'HIGH'",
"code": "invalid_enum_value"
}
],
"requestId": "5b7d9f11-2c4e-4638-8a0b-1d3f5a7c9e02"
}Too Many Requests
You exhausted a per-tenant rate bucket (read, write, or ai). The response includes a Retry-After header (seconds) plus the X-RateLimit-* family, and a rateLimit object in the body. Honour Retry-After; it is authoritative.
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 12
X-RateLimit-Class: write
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1746123456
x-request-id: 0c1d2e3f-4a5b-4c6d-8e9f-a0b1c2d3e4f5
{
"type": "https://atlas.dev/errors/rate-limited",
"title": "Rate limit exceeded",
"status": 429,
"detail": "Too many write requests for this tenant. Retry in 12s.",
"rateLimit": {
"class": "write",
"limit": 60,
"windowMs": 60000,
"retryAfterSec": 12,
"tier": "pro"
},
"requestId": "0c1d2e3f-4a5b-4c6d-8e9f-a0b1c2d3e4f5"
}Internal Server Error
Something failed on our side. The body is deliberately generic - it never leaks internal detail - but the requestId (also in x-request-id) lets support trace the exact failure. Safe to retry with exponential backoff (300ms, 800ms, 2s).
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
x-request-id: a1b2c3d4-e5f6-4708-9a0b-1c2d3e4f5061
{
"type": "about:blank",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred.",
"requestId": "a1b2c3d4-e5f6-4708-9a0b-1c2d3e4f5061"
}Rate limits
Every route is classified read, write, or ai. Each class has its own per-tenant bucket. The ceilings below are defaults; your tenant may have a higher limit configured.
| Class | Default ceiling | Applies to |
|---|---|---|
read | 300/m | GET requests on /v1/* (excluding /v1/ai/*) |
write | 60/m | POST/PATCH/PUT/DELETE on /v1/* |
ai | 20/m | Any /v1/ai/* call (read or write) |
429 response shape
Retry-After (seconds), X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix seconds when the bucket resets). Honour Retry-After; it is authoritative.Idempotency
Every POST route accepts an Idempotency-Key header. Replaying the same key (same tenant) within 24h returns the original 2xx response verbatim, including the resource id. This is what makes retries safe on flaky networks.
curl -X POST https://api-atlas.wrxstack.com/v1/tasks \
-H "Authorization: Bearer atlas_pat_REPLACE_ME" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"projectId": "prj_...",
"title": "Review Q3 forecast",
"priority": "HIGH",
"dueOn": "2026-05-01T17:00:00Z"
}'Use a fresh UUID per logical operation: uuidgen on the shell, randomUUID() in Node, or any stable hash you can re-derive on retry. Do not reuse a key for different requests; Atlas matches on the key, not the body.
Pagination
List endpoints return a cursor. Pass nextCursor back as ?cursor= to get the following page. Cursors are opaque, so never parse or modify them.
# First page
curl -H "Authorization: Bearer atlas_pat_..." \
"https://api-atlas.wrxstack.com/v1/tasks?limit=50"
# { "items": [...], "nextCursor": "eyJpZCI6Li4ufQ==" }
# Next page
curl -H "Authorization: Bearer atlas_pat_..." \
"https://api-atlas.wrxstack.com/v1/tasks?limit=50&cursor=eyJpZCI6Li4ufQ=="Webhooks
Subscribe delivery URLs via /v1/webhooks. Atlas signs every payload with HMAC-SHA256 and retries failed deliveries automatically on a fixed schedule (60s, 5m, 30m, 2h, 12h).
POST https://your.app/atlas-webhook
content-type: application/json
x-atlas-event: task.created
x-atlas-webhook-signature: 3b8f...e21a
x-atlas-webhook-timestamp: 1783412096000
x-atlas-webhook-id: whd_5a1b2c3d
x-atlas-delivery: evt_01HW3T:0Verify the canonical x-atlas-webhook-signature header, subscribe to events, and replay deliveries: the dedicated Webhooks guide covers events, payloads and headers, signature verification in five languages, and the full retry schedule end to end.
SDKs and clients
The official TypeScript client mirrors the REST surface 1:1. For AI agents, the MCP server wraps the same endpoints in a model-friendly tool catalogue.
import { createAtlasClient } from '@atlas/client';
const atlas = createAtlasClient({
baseUrl: 'https://api-atlas.wrxstack.com',
// PATs are passed verbatim, no refresh logic needed.
getAccessToken: () => process.env.ATLAS_API_KEY ?? null,
});
const { items } = await atlas.tasks.list({ limit: 50 });
const created = await atlas.tasks.create({
projectId: 'prj_...',
title: 'Review Q3 forecast',
priority: 'HIGH',
});Building an AI agent integration? See the MCP setup guide for Claude Desktop, Cursor, and Cline configuration.
Downloads
All three artefacts are generated server-side from the same hand-written OpenAPI 3.1 source, so nothing is ever stale.
- OpenAPI 3.1 specMachine-readable spec with x-required-scopes, x-ratelimit-class, and x-rate-limits extensions.openapi.json
- Postman collectionPostman v2.1, grouped by tag, with bearer auth, Idempotency-Key, and sample bodies pre-wired.postman.json
- Postman environmentCompanion environment template. Paste your PAT into apiKey and you are sending real requests.environment.json
FAQ
- Can I use a session cookie or JWT instead of a PAT?
- Yes, the bearer header accepts either. JWT-authenticated calls bypass the scope gate (the session is implicitly all-scopes). PATs are still recommended for server-to-server because they are tenant-scoped, scope-narrowed, and revocable independently of any user session.
- Why does my POST occasionally return the same id twice?
- You sent the same
Idempotency-Keytwice within 24h. That is by design: the second call returns the original 2xx response so you do not double-create. Use a fresh UUID for each logical create. - How do I update a task without overwriting concurrent edits?
- Pass the task's current
versionfield asIf-Match: <version>. If the task changed in the meantime you get a 409; re-read, merge, and retry. - Is there a sandbox or staging environment?
- Self-host Atlas with a separate Postgres for sandbox. The same OpenAPI spec applies; just point your PAT-minting client and
baseUrlat the sandbox URL. - How do I get notified when the spec changes?
- Watch the release notes; every public-API change is documented there. The spec also bumps
info.versionon breaking changes.