SDK
Go SDK
A dependency-free client for the Atlas REST API, generated from the OpenAPI spec. Works in any Go 1.22+ project.
Overview
The Atlas Go SDK wraps the same REST surface documented in the API guide. NewClient returns a *Client with a method per operation; methods return decoded JSON as any and a typed error on non-2xx responses. It is generated from the OpenAPI document the API serves at /v1/openapi.json, so method names track the API exactly.
Method naming
operationId. Listing tasks (operation getTasks) is client.GetTasks(...). The methods overview maps the common resources.Install
Atlas does not publish this client as a Go module. Download the single file from Atlas itself; it needs nothing beyond the standard library.
# Download the single-file client into a package folder of your module.
mkdir -p atlas && curl -fsSL -o atlas/atlas.go https://atlas.wrxstack.com/sdk/go/atlas.go
# Check it arrived intact.
curl -fsSL -o atlas.go.sha256 https://atlas.wrxstack.com/sdk/go/atlas.go.sha256
(cd atlas && shasum -a 256 -c ../atlas.go.sha256)Use only the file from Atlas
Authentication
Every request carries a Personal Access Token as a bearer credential. Tokens start with atlas_pat_ and are scope-bounded and revocable.
package main
import (
"log"
"os"
// The folder you downloaded the client into, inside your own module.
"your.module/path/atlas"
)
func main() {
// Read the token from the environment - never hard-code it in source.
client, err := atlas.NewClient(os.Getenv("ATLAS_TOKEN"))
if err != nil {
log.Fatal(err)
}
_ = client
}Scopes are enforced server-side
403 (insufficient_scope) and returns a *ForbiddenError; the client never widens a token. Create a token with the narrowest scopes that works and widen only when a call is refused.Quickstart
One authenticated request, end to end, continuing from the client above. Set ATLAS_TOKEN in your environment first.
res, err := client.GetTasks(map[string]string{"limit": "5"})
if err != nil {
log.Fatal(err)
}
// List endpoints answer an { "items": [...], "nextCursor": ... } envelope.
page := res.(map[string]any)
for _, item := range page["items"].([]any) {
task := item.(map[string]any)
fmt.Println(task["id"], task["title"])
}Core examples
The everyday task and project operations. Reads answer an { items, nextCursor } envelope decoded as map[string]any.
List tasks with filters
// Filters map straight to the REST query string; query values are strings.
res, err := client.GetTasks(map[string]string{
"projectId": "prj_123",
"status": "IN_PROGRESS",
"assigneeId": "usr_42",
"limit": "50",
})
if err != nil {
log.Fatal(err)
}
tasks := res.(map[string]any)["items"].([]any)Get a task
// Path parameters are the leading arguments, filled in and URL-encoded.
res, err := client.GetTasksById(taskID, nil)
if err != nil {
log.Fatal(err)
}
task := res.(map[string]any)
fmt.Println(task["title"], task["status"])Path parameters
GetTasksById(taskID, nil) calls /v1/tasks/{id} with the id URL-encoded into the path. The same holds for update and delete.Create a task
created, err := client.PostTasks(map[string]any{
"projectId": "prj_123",
"title": "Review Q3 forecast",
"priority": "HIGH",
"dueOn": "2026-05-01T17:00:00Z",
}, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("created", created.(map[string]any)["id"])Update a task
updated, err := client.PatchTasksById(taskID, map[string]any{"status": "DONE"}, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(updated.(map[string]any)["status"])Cursor-based pagination
func iterTasks(client *atlas.Client, filters map[string]string) ([]any, error) {
var all []any
cursor := ""
for {
q := map[string]string{"limit": "100"}
for k, v := range filters {
q[k] = v
}
if cursor != "" {
q["cursor"] = cursor
}
res, err := client.GetTasks(q)
if err != nil {
return nil, err
}
page := res.(map[string]any)
if items, ok := page["items"].([]any); ok {
all = append(all, items...)
}
next, _ := page["nextCursor"].(string)
if next == "" {
return all, nil
}
cursor = next
}
}List projects
res, err := client.GetProjects(map[string]string{"limit": "50"})
if err != nil {
log.Fatal(err)
}
for _, item := range res.(map[string]any)["items"].([]any) {
project := item.(map[string]any)
fmt.Println(project["id"], project["name"])
}Error handling
Non-2xx responses return a typed error. Match it with errors.As; every typed error embeds *ApiError, so Status, Message, and Body are always available.
_, err := client.PostTasks(map[string]any{"title": ""}, nil)
var forbidden *atlas.ForbiddenError
var unauthorized *atlas.UnauthorizedError
var apiErr *atlas.ApiError
switch {
case errors.As(err, &forbidden):
// 403: the token is missing a scope. Body is the parsed problem+json.
if body, ok := forbidden.Body.(map[string]any); ok {
fmt.Println("need scopes:", body["requiredScopes"])
}
case errors.As(err, &unauthorized):
// 401: token invalid or revoked - re-authenticate.
log.Fatal("unauthorized")
case errors.As(err, &apiErr):
// 409 / 422 / 429: no dedicated type, so branch on Status.
fmt.Println(apiErr.Status, apiErr.Message)
}| HTTP status | Error class | Meaning |
|---|---|---|
| 400 | *atlas.BadRequestError | Malformed request or query. |
| 401 | *atlas.UnauthorizedError | Token missing, invalid, expired, or revoked. |
| 403 | *atlas.ForbiddenError | Token is missing a required scope. |
| 404 | *atlas.NotFoundError | Resource does not exist or is out of tenant. |
| 409, 422, 429 | ApiError (base) | No dedicated class: branch on the status code and read the body. |
| 5xx | *atlas.ServerError | Transient server fault. Safe to retry. |
Only the statuses above get a dedicated type. Conflicts (409), validation failures (422), and rate limits (429) return the base *ApiError, so branch on Status and read Body. Every error wraps the API's RFC 9457 problem+json envelope:
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"
}Body decodes to that JSON object: type, title, status, detail, and a requestId to quote in support tickets. A 422 adds an errors array; a 429 adds a rateLimit object.
Retries and rate limits
Retry 429 and 5xx; never retry 4xx you caused. The 429 body carries the retry delay, so honour it before falling back to backoff.
var retryable = map[int]bool{429: true, 500: true, 502: true, 503: true, 504: true}
// retryDelay reports how long to wait, or ok=false to stop retrying.
func retryDelay(err error, attempt int) (time.Duration, bool) {
var server *atlas.ServerError
var apiErr *atlas.ApiError
switch {
case errors.As(err, &server):
return backoff(attempt), true // 5xx: transient
case errors.As(err, &apiErr) && apiErr.Status == 429:
// The 429 body carries the authoritative retry delay, in seconds.
if body, ok := apiErr.Body.(map[string]any); ok {
if rl, ok := body["rateLimit"].(map[string]any); ok {
if secs, ok := rl["retryAfterSec"].(float64); ok {
return time.Duration(secs) * time.Second, true
}
}
}
return backoff(attempt), true
default:
return 0, false
}
}
func backoff(attempt int) time.Duration {
d := time.Duration(1<<uint(attempt-1)) * time.Second
if d > 30*time.Second {
d = 30 * time.Second
}
return d + time.Duration(rand.Intn(1000))*time.Millisecond // jitter
}
func withRetries[T any](call func() (T, error), maxAttempts int) (T, error) {
for attempt := 1; ; attempt++ {
res, err := call()
if err == nil {
return res, nil
}
delay, ok := retryDelay(err, attempt)
if !ok || attempt == maxAttempts {
var zero T
return zero, err
}
time.Sleep(delay)
}
}Back off with jitter
retryAfterSec is present, use exponential backoff with a random jitter so a fleet of clients does not retry in lockstep. Cap total attempts: a persistent 429 means you need a higher tier, not more retries.Idempotency
Every write accepts an Idempotency-Key header. Replaying the same key within 24h returns the original response verbatim: safe retries on flaky networks.
// The generated client sends a fixed header set and takes no per-call context,
// so attach an Idempotency-Key by composing the write directly. Reuse one key
// across every retry so the API replays (24h) instead of duplicating.
func createTask(ctx context.Context, token, key string, body map[string]any) (map[string]any, error) {
payload, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(
ctx, http.MethodPost, "https://api-atlas.wrxstack.com/v1/tasks", bytes.NewReader(payload),
)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var created map[string]any
return created, json.NewDecoder(resp.Body).Decode(&created)
}Reuse the key on retry
Configuration
NewClient takes functional options. WithBaseURL retargets a staging server; WithHTTPClient supplies your own timeout, transport, and headers.
// headerTransport adds a static header to every request the client makes.
type headerTransport struct {
base http.RoundTripper
headers map[string]string
}
func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
for k, v := range t.headers {
req.Header.Set(k, v)
}
return t.base.RoundTrip(req)
}
func newClient(token string) (*atlas.Client, error) {
httpClient := &http.Client{
Timeout: 30 * time.Second,
Transport: &headerTransport{
base: http.DefaultTransport,
headers: map[string]string{"X-Request-Source": "atlas-cli"},
},
}
return atlas.NewClient(
token,
atlas.WithBaseURL("https://api-atlas.wrxstack.com"), // the default; override for staging
atlas.WithHTTPClient(httpClient),
)
}A custom http.RoundTripper is the place to add a static header (a request source, a trace id) or wire in your own retry and logging middleware for every call.
Methods overview
The common task and project operations. The full set (one method per operation) lives in the API reference.
| Resource | REST | SDK method |
|---|---|---|
| List tasks | GET /v1/tasks | GetTasks(query) |
| Create task | POST /v1/tasks | PostTasks(body, query) |
| Get task | GET /v1/tasks/{id} | GetTasksById(id, query) |
| Update task | PATCH /v1/tasks/{id} | PatchTasksById(id, body, query) |
| Delete task | DELETE /v1/tasks/{id} | DeleteTasksById(id, query) |
| List projects | GET /v1/projects | GetProjects(query) |
| Create project | POST /v1/projects | PostProjects(body, query) |
Next steps
The SDK is a thin, faithful layer over the REST API. When you need the exact shape of a request or response, go to the source.
- REST API guideAuth, pagination, idempotency, and rate limits at the protocol level: the contract every SDK speaks./docs/api
- API referenceEvery operation, request body, and response shape: the source of truth for method arguments./docs/api/reference
- MCP for AI agentsHand the same operations to an AI agent over the Model Context Protocol./docs/mcp