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. Every type comes from one registry: one shared type per kind of failure, such as not-found, forbidden and conflict, with the specific case in code. 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, always https://atlas.dev/errors/<slug>. A module-specific not found, forbidden or conflict is served as the shared type, with its own name in code. |
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, stale If-Match version, or a connector that is not connected. | Yes, re-read then retry; not for a connector |
| 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": "https://atlas.dev/errors/bad-request",
"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). A session token that has only expired answers with type https://atlas.dev/errors/token-expired, so refresh it; every other case is https://atlas.dev/errors/unauthenticated.
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
x-request-id: 7c9e2f14-3a5b-4d6e-8f70-1b2c3d4e5f60
{
"type": "https://atlas.dev/errors/unauthenticated",
"title": "Not signed in",
"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. Any other refusal uses type https://atlas.dev/errors/forbidden.
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": "https://atlas.dev/errors/not-found",
"code": "task-not-found",
"title": "Task not found",
"status": 404,
"detail": "No task with id tsk_9 in this workspace.",
"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. A connector route also answers 409 when the service it reaches is not connected (connector-not-connected) or its connection has expired and must be made again (connector-reconnect-required). Retrying does not help there: an administrator connects the service in Settings, under Integrations. The body names it in connectorId. Your token is fine in both cases, so do not refresh it.
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
x-request-id: 8e0c2a46-9f7b-4d31-a0c5-2e4f6a8b0d13
{
"type": "https://atlas.dev/errors/conflict",
"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": "https://atlas.dev/errors/internal",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred.",
"requestId": "a1b2c3d4-e5f6-4708-9a0b-1c2d3e4f5061"
}Rate limits
Every route is classified as read, write, or AI, and each class has its own limit of requests a minute for each workspace. Your plan sets the limits below, and Atlas can set different ones for your workspace.
| 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) |
Requests a minute by plan
| Plans | Read requests a minute | Write requests a minute | AI requests a minute |
|---|---|---|---|
| Free | 300 | 60 | 20 |
| Pro, Team, and Business | 1,200 | 300 | 60 |
| Enterprise | 6,000 | 1,500 | 300 |
A limit belongs to the workspace and is shared by every API instance, so spreading calls across servers or tokens does not raise it. A limit Atlas sets for a workspace replaces the plan limit.
Rate limit headers
Every answer to a limited call carries these headers. When a limit is reached, Atlas answers 429 with a Retry-After header in seconds. Wait that long before you try again.
| Header | Meaning |
|---|---|
X-RateLimit-Class | The class the call was counted in: read, write, ai. |
X-RateLimit-Limit | The requests allowed in the window. |
X-RateLimit-Remaining | The requests left in the current window. |
X-RateLimit-Reset | When the window starts again, in seconds since 1 January 1970, in UTC. |
X-RateLimit-Window-Ms | The length of the window in milliseconds. |
X-RateLimit-Tier | The rate limit tier of the workspace: free, pro, enterprise. |
Monthly quota
Calls made with a personal access token or an OAuth token count toward a monthly quota. Your plan sets the quota, and Atlas can set a different one for your workspace. The count starts again at the beginning of each calendar month, in UTC. Calls from the signed-in Atlas app are not counted.
When the quota has a cap, each answer to a token call carries X-Quota-Limit, X-Quota-Remaining, and X-Quota-Reset. X-Quota-Reset gives the moment the count starts again, in seconds since 1 January 1970, in UTC.
When the quota is spent, Atlas answers 429 with the problem type https://atlas.dev/errors/quota-exceeded, a Retry-After header, and a quota member that gives the period, the limit, the calls used, and when the count starts again.
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 432000
X-Quota-Limit: 100000
X-Quota-Remaining: 0
X-Quota-Reset: 1793491200
x-request-id: 6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d
{
"type": "https://atlas.dev/errors/quota-exceeded",
"title": "Monthly API quota reached",
"status": 429,
"detail": "This workspace has used its 100000 API calls for the month. The count starts again at 2026-11-01T00:00:00.000Z.",
"quota": {
"period": "month",
"limit": 100000,
"used": 100001,
"resetAt": "2026-11-01T00:00:00.000Z"
},
"requestId": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d"
}Read your limits
Atlas can adjust the limits of a single workspace: the requests a minute for each class, the monthly quota, the daily AI spend cap, and how its webhooks are retried.
Read the limits in force for your workspace with GET /v1/workspace/limits. Owners and administrators can call it, and so can a token with the workspace:read scope. The answer gives each value in force, the plan default, any value Atlas has set, and the calls counted this month.
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
Every list endpoint returns { items, nextCursor }, never a bare array. Pass nextCursor back as ?cursor= to get the following page; it is null on the last page, and a list that returns all of its rows at once always answers null. 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"
# Response: { "items": [...], "nextCursor": "eyJpZCI6Li4ufQ==" }
# Next page
curl -H "Authorization: Bearer atlas_pat_..." \
"https://api-atlas.wrxstack.com/v1/tasks?limit=50&cursor=eyJpZCI6Li4ufQ=="limit is 1 to 100, 50 by default. A list that can be sorted takes sort, naming one of the fields the operation lists, and order=asc or order=desc; the cursor belongs to that sort, so start again without a cursor when you change it. A cursor that cannot be read, or belongs to another sort, is refused with 422. Rows are ordered by the sort field and then by id, so a row deleted between two requests never ends the list early.
Versioning and changes
The API is changed in place, with no version header and no old names kept as aliases. Every change is listed in the changelog on the day it ships, with the old shape, the new shape, and the operations it affects. A webhook delivery carries the apiVersion of its envelope, so a receiver can tell which shape it is reading.
To stay safe, ignore fields you do not recognise, branch on the problem type rather than on its wording, and read the developer notes in the changelog before you upgrade an integration. Changelog
Webhooks
Subscribe an address with /v1/webhooks. Atlas signs every delivery with HMAC-SHA256 and, when a delivery fails, tries again after 30s, 2m, 10m, 30m, 2h, 8h, 24h.
POST https://your.app/atlas-webhook
content-type: application/json
user-agent: Atlas-Webhooks/2
atlas-signature: t=1790933412,v1=<64 hexadecimal characters>
atlas-timestamp: 1790933412
atlas-event-id: evt_cm1b7x0d80000lq8zf2a1k9e3
atlas-event-type: task.created
atlas-delivery-id: cm1b7x2k40003lq8z5d0v9h2e
atlas-subscription-id: cm1a4r9t20001lq8z3c7m6n1pVerify the Atlas-Signature header on every delivery before you trust it. 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 with typehttps://atlas.dev/errors/conflictand codeversion-conflict; re-read, merge, and retry. - Is there a sandbox or staging environment?
- Self-host Atlas with a separate database for a 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.