AtlasWork, planned itself.

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

All systems operational
  • SOC 2 II
  • ISO 27001
  • HIPAA
  • GDPR

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
Skip to documentation
Docs
Back to Atlas

Start here

  • Overview

Developer

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

Webhooks

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

Connect

  • Connectors
  • Integrations

Product

  • Collaboration and chat

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 docs/atlas-openapi.json, so method names track the API exactly.

Method naming

Each method is the PascalCase of the OpenAPI operationId. Listing tasks (operation atlas_get_tasks) is client.AtlasGetTasks(...). The methods overview maps the common resources.

Install

Fetch the module with go get. It pulls in nothing beyond the standard library.

bash
go get github.com/atlas-app/atlas-go-sdk

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"

	atlas "github.com/atlas-app/atlas-go-sdk"
)

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 SDK never widens a token. Mint the narrowest scope set 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.AtlasGetTasks(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.AtlasGetTasks(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
// Single-resource routes carry the id in the path. The typed method does not
// interpolate it, so compose these with the standard library and your token.
func getTask(ctx context.Context, token, id string) (map[string]any, error) {
	req, err := http.NewRequestWithContext(
		ctx, http.MethodGet, "https://api.atlas.app/v1/tasks/"+id, nil,
	)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Accept", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var task map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&task); err != nil {
		return nil, err
	}
	return task, nil
}

Single-resource routes

The generated AtlasGetTasksRecord targets /tasks/{id} but does not interpolate the id, and the transport takes no per-call context. Compose these with net/http as shown; the same applies to update and delete.

Create a task

go
created, err := client.AtlasPostTasks(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
// PATCH /tasks/{id} - a single-resource write, composed like the GET above.
payload, _ := json.Marshal(map[string]any{"status": "DONE"})
req, _ := http.NewRequest(
	http.MethodPatch, "https://api.atlas.app/v1/tasks/"+taskID, bytes.NewReader(payload),
)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)

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.AtlasGetTasks(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.AtlasGetProjects(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.AtlasPostTasks(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.app/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 staging or self-hosted; 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.app/v1"), // staging or self-hosted
		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 /tasksAtlasGetTasks(query)
Create taskPOST /tasksAtlasPostTasks(body, query)
Get taskGET /tasks/{id}AtlasGetTasksRecord *
Update taskPATCH /tasks/{id}AtlasPatchTasksRecord *
Delete taskDELETE /tasks/{id}AtlasDeleteTasksRecord *
List projectsGET /projectsAtlasGetProjects(query)
Create projectPOST /projectsAtlasPostProjects(body, query)

* Single-resource methods exist but send the literal {id} path; compose them with net/http (see Get a task).


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