AtlasWork, planned itself.

    The AI-native, all-in-one work platform. Tasks, projects, CRM, contracts, and analytics in one calm workspace.

    System status
    • SSO
    • SCIM
    • Two-factor sign-in
    • Audit log

    Product

    • Overview
    • PDF tools
    • Diagram tools
    • People & HR
    • Integrations
    • Marketplace
    • Pricing

    Resources

    • Guides
    • Glossary
    • Compare
    • Docs
    • API reference
    • Support
    • Changelog
    • Status

    Company

    • About
    • Careers
    • Press
    • Contact

    Legal & trust

    • Trust center
    • Security
    • Privacy
    • Terms
    • DPA
    • GDPR
    • SLA
    • Refunds
    • Google API data
    Atlas, a product by wrxstack.com·© 2026 wrxstack·All rights reserved
    PrivacyTermsSecurityStatus

    You are in control of your cookies

    Atlas uses strictly necessary cookies to keep you signed in. With your consent, we add anonymized product analytics, conversion attribution, and remembered preferences. Change your mind any time at /privacy/cookies.

    Off until you agree · Change it any time

    • Necessaryalways on
    • Analyticsopt-in
    • Marketingopt-in
    • Preferencesopt-in
    Skip to documentation
    Docs
    Back to Atlas

    Start here

    • Overview

    Developer

    • REST API guide
    • Authentication
    • API reference
    • MCP (AI agents)
    • MCP tools reference
    • SDKs
    • Quick actions
    • Changelog

    Webhooks

    • Overview
    • Quickstart
    • Events
    • Payloads and headers
    • Security and signing
    • Delivery and retries
    • Managing via API

    Connect

    • Connectors
    • Integrations

    Product

    • Collaboration and chat
    • Signing in and security
    • Client portal

    Reference

    • Glossary
    • Keyboard shortcuts
    • Module reference

    SDK

    Go SDK

    A dependency-free client for the Atlas REST API, generated from the OpenAPI spec. Works in any Go 1.22+ project.

    Language
    PythonGoRuby
    One REST surface, three idiomatic clients.

    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

    Each method is the PascalCase of the OpenAPI 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.

    bash
    # 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

    No Go module on a public proxy is published by Atlas. Download the client from atlas.wrxstack.com, and download it again when the API changes.

    Authentication

    Every request carries a Personal Access Token as a bearer credential. Tokens start with atlas_pat_ and are scope-bounded and revocable.

    go
    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

    A token only reaches the routes its scopes allow. A call outside them fails with 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.

    go
    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

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

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

    Every route with a path parameter takes it as a leading argument: 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

    go
    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

    go
    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

    go
    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

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

    go
    _, 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 statusError classMeaning
    400*atlas.BadRequestErrorMalformed request or query.
    401*atlas.UnauthorizedErrorToken missing, invalid, expired, or revoked.
    403*atlas.ForbiddenErrorToken is missing a required scope.
    404*atlas.NotFoundErrorResource does not exist or is out of tenant.
    409, 422, 429ApiError (base)No dedicated class: branch on the status code and read the body.
    5xx*atlas.ServerErrorTransient 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
    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.

    go
    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

    When no 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.

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

    Generate the key once per logical operation and pass the same value on every retry. A new key is a new operation, so a retried create with a fresh key produces a duplicate. See the REST guide for the full replay semantics.

    Configuration

    NewClient takes functional options. WithBaseURL retargets a staging server; WithHTTPClient supplies your own timeout, transport, and headers.

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

    ResourceRESTSDK method
    List tasksGET /v1/tasksGetTasks(query)
    Create taskPOST /v1/tasksPostTasks(body, query)
    Get taskGET /v1/tasks/{id}GetTasksById(id, query)
    Update taskPATCH /v1/tasks/{id}PatchTasksById(id, body, query)
    Delete taskDELETE /v1/tasks/{id}DeleteTasksById(id, query)
    List projectsGET /v1/projectsGetProjects(query)
    Create projectPOST /v1/projectsPostProjects(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

    On this page

    • Overview
    • Install
    • Authentication
    • Quickstart
    • Core examples
    • List tasks
    • Get a task
    • Create a task
    • Update a task
    • Pagination
    • List projects
    • Error handling
    • Retries and rate limits
    • Idempotency
    • Configuration
    • Methods overview
    • Next steps