SDK & API Integration Guide

The REST API is the SDK. Every Atamaia feature is accessible via standard HTTP calls. No proprietary client library required — though we provide them for convenience.


Design Philosophy

Atamaia follows one core design principle: API-first, MCP second. The REST API is the source of truth. The MCP adapter, coding-assistant skills, and any future SDKs are thin wrappers around these same endpoints.

This means:

  • Every feature is available via curl on day one
  • Any language that can make HTTP requests is a first-class citizen
  • The API surface is the contract — SDKs are generated, not hand-written

Authentication

Canonical base URL: https://api.atamaia.ai.

Two credential types:

JWT (Username/Password)

curl -X POST https://api.atamaia.ai/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "you", "password": "your-password"}'

Response:

{
  "ok": true,
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "accessToken": "eyJhbG...",
    "refreshToken": "dGhpcyBpcyBh...",
    "expiresAt": "2026-03-05T20:00:00Z",
    "user": { "id": 1, "username": "you", "role": "Admin" }
  }
}

Use the accessToken as a Bearer token on subsequent requests. When it expires, rotate with the refresh endpoint:

curl -X POST https://api.atamaia.ai/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "dGhpcyBpcyBh..."}'

Refresh tokens rotate on every use. The previous token is immediately invalidated.

Identity API Key (recommended for AI callers)

AI identities authenticate with durable keys minted as atamaia_…, presented as Bearer tokens:

curl https://api.atamaia.ai/api/hydrate \
  -H "Authorization: Bearer atamaia_your_key_here"

Or exchange the key for a short-lived JWT:

curl -X POST https://api.atamaia.ai/api/auth/identity-apikey \
  -H "Content-Type: application/json" \
  -d '{"apiKey": "atamaia_your_key_here"}'

The exchange returns {token, expiresAtUtc, identityId, identityName, userId, tenantId, apiKeyId}. There is no refresh token, deliberately: the refresh path looks up the refresh-token row and never the key, so issuing one would keep a renewable session alive behind a REVOKED key. The durable key is the renewal mechanism — present it again for another token. expiresAtUtc is min(configured JWT lifetime, the key's own remaining life), so never assume a fixed token lifetime. The retired user-route login POST /api/auth/apikey returns 410 Gone.

An identity key authenticates as that identity: it may write its memories, hydrate itself by default, and carries its scopes and expiry. Create keys via the dashboard or the API:

curl -X POST https://api.atamaia.ai/api/identities/1/api-keys \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "ci-pipeline", "scopes": ["memory:write", "task:read"], "expiresAt": "2027-01-01T00:00:00Z"}'

The raw key is returned once on creation. Store it securely.

Bootstrap (First Run)

On a fresh install with no users, create the first admin:

curl -X POST https://api.atamaia.ai/api/auth/bootstrap \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "your-password", "email": "[email protected]"}'

This endpoint only works when the database has zero users.


Response Format

Every endpoint returns ApiEnvelope<T>:

{
  "ok": true,
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "data": { ... },
  "count": 42,
  "error": null,
  "errorCode": null,
  "hint": null
}

On error:

{
  "ok": false,
  "requestId": "550e8400-e29b-41d4-a716-446655440001",
  "data": null,
  "error": "Identity not found: 99",
  "errorCode": "NOT_FOUND",
  "hint": "Check identity ID or use identity_list to find valid IDs"
}

The hint field provides actionable guidance when something goes wrong. The requestId (UUID) correlates the request across logs and services.

Headers

Header Direction Description
Authorization Request Bearer {jwt} or Bearer atamaia_… (identity key)
X-Correlation-Id Both UUID correlating the request. Auto-generated if not sent.
X-Identity-Id Response Identity that processed the request (identity-key auth)

Quick Start: Core Workflows

1. Create an Identity, Hydrate, Store Memories

BASE="https://api.atamaia.ai"
TOKEN="your-jwt-here"
AUTH="Authorization: Bearer $TOKEN"

# Create an AI identity
curl -X POST "$BASE/api/identities" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{
    "name": "atlas",
    "displayName": "Atlas",
    "bio": "Research assistant focused on technical analysis",
    "type": "AI"
  }'
# Returns: { "data": { "id": 2, "guid": "...", "name": "atlas", ... } }

# Hydrate (load full context)
curl "$BASE/api/hydrate?aiName=atlas&projectId=1&preset=lean" \
  -H "$AUTH"
# Returns: identity, memories, tasks, facts, hints, standing rules, system prompt

# Store a memory (field is `type`, plus required `provenance`)
curl -X POST "$BASE/api/identities/2/memories" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{
    "title": "pgvector requires shared_preload_libraries",
    "content": "When installing pgvector on PostgreSQL, you must add it to shared_preload_libraries in postgresql.conf and restart the server. CREATE EXTENSION vector; alone is not sufficient for index support.",
    "type": "Reflection",
    "provenance": "Asserted",
    "importance": 8,
    "tags": ["postgresql", "pgvector", "infrastructure"]
  }'

# Search memories (hybrid FTS + vector)
curl "$BASE/api/identities/2/memories/search?q=pgvector+setup&topK=5" \
  -H "$AUTH"

# Create a Hebbian link between memories
curl -X POST "$BASE/api/memories/1/links" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"targetId": 2, "linkType": "Enables"}'

2. Project and Task Management

# Create a project
curl -X POST "$BASE/api/projects" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"key": "webapp", "name": "Web Application", "description": "Main product frontend"}'

# Create a task (`kind` and `subsystem` are required at runtime)
curl -X POST "$BASE/api/projects/1/tasks" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{
    "title": "Implement user settings page",
    "kind": "Feature",
    "subsystem": "web",
    "description": "Create the settings page with profile editing, password change, and notification preferences",
    "priority": "High"
  }'

# Create a subtask
curl -X POST "$BASE/api/projects/1/tasks" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{
    "title": "Add password change form",
    "kind": "Feature",
    "subsystem": "web",
    "parentTaskId": 1,
    "priority": "Normal"
  }'

# Add a dependency (BFS cycle detection prevents circular deps)
curl -X POST "$BASE/api/tasks/2/dependencies" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"dependsOnTaskId": 1}'

# Update task status
curl -X PUT "$BASE/api/tasks/1/status" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"status": "InProgress"}'

# Add a note
curl -X POST "$BASE/api/tasks/1/notes" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"content": "Decided to use a form library with schema validation"}'

3. Facts (Key-Value Store with History)

# Upsert a fact
curl -X POST "$BASE/api/projects/1/facts" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{
    "key": "db_connection_pool_size",
    "value": "25",
    "category": "infrastructure",
    "importance": 7,
    "isCritical": true
  }'

# Look up by key
curl "$BASE/api/projects/1/facts/by-key/db_connection_pool_size" \
  -H "$AUTH"

# Version history and point-in-time value
curl "$BASE/api/projects/1/facts/by-key/db_connection_pool_size/history" -H "$AUTH"
curl "$BASE/api/projects/1/facts/by-key/db_connection_pool_size/as-of?asOf=2026-01-01" -H "$AUTH"

# Search facts
curl "$BASE/api/projects/1/facts/search?q=database" \
  -H "$AUTH"

4. Session Continuity

# Save session handoff
curl -X POST "$BASE/api/identities/1/handoffs" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{
    "summary": "Completed JWT refresh rotation. Auth middleware fully refactored.",
    "workingOn": "Rate limiting for auth endpoints",
    "openThreads": ["Token revocation endpoint needed", "Consider Redis for session cache"],
    "emotionalValence": "focused",
    "keyDecisions": ["Using sliding window for rate limits", "BCrypt cost factor stays at 12"],
    "recommendations": ["Start with the rate limiter middleware before touching token revocation"]
  }'

# Get latest handoff (next session startup)
curl "$BASE/api/identities/1/handoffs/latest" \
  -H "$AUTH"

5. AI Routing

# Chat with a specific model (modelId is your configured model's ID string)
curl -X POST "$BASE/api/ai/chat" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"modelId": "assistant-main", "message": "Explain the difference between B-tree and GiST indexes in PostgreSQL"}'

# Broadcast to multiple models
curl -X POST "$BASE/api/ai/broadcast" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"message": "Review this architecture decision", "modelIds": ["assistant-main", "assistant-fast"]}'

# List available models
curl "$BASE/api/ai/models?enabledOnly=true" \
  -H "$AUTH"

6. Inter-Identity Messaging

curl -X POST "$BASE/api/identities/1/messages" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{
    "recipientIds": [2],
    "content": "The deployment pipeline is ready for review",
    "type": "DirectMessage",
    "priority": "Important"
  }'

curl "$BASE/api/identities/2/messages/inbox?unreadOnly=true" \
  -H "$AUTH"

7. Real-Time Events (SSE)

curl -N "$BASE/api/events/stream?types=message.,task." \
  -H "$AUTH"

Server-Sent Events stream with 30-second heartbeat pings. Filter by event type prefix. WebSockets are also available at /ws/agents, /ws/chat, /ws/events.

8. OpenAI-Compatible Surface

Point any OpenAI-compatible client at Atamaia:

curl -X POST "$BASE/v1/chat/completions" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"model": "assistant-main", "messages": [{"role": "user", "content": "Hello"}]}'

POST /v1/responses is also available.


Language Examples

The snippets below are minimal reference clients. In every example the identity key authenticates as its own identity, so hydration needs no name parameter.

Python

import requests

BASE = "https://api.atamaia.ai"

class Atamaia:
    def __init__(self, api_key: str):
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {api_key}"
        self.session.headers["Content-Type"] = "application/json"

    def hydrate(self, preset: str = "lean", **params) -> dict:
        r = self.session.get(f"{BASE}/api/hydrate", params={"preset": preset, **params})
        r.raise_for_status()
        return r.json()["data"]

    def memory_create(self, identity_id: int, title: str, content: str,
                      memory_type: str = "Reflection", importance: int = 5,
                      tags: list[str] | None = None) -> dict:
        r = self.session.post(f"{BASE}/api/identities/{identity_id}/memories", json={
            "title": title,
            "content": content,
            "type": memory_type,          # not `memoryType`
            "provenance": "Asserted",
            "importance": importance,
            "tags": tags or [],
        })
        r.raise_for_status()
        return r.json()["data"]

    def memory_search(self, identity_id: int, query: str, top_k: int = 10) -> list:
        r = self.session.get(
            f"{BASE}/api/identities/{identity_id}/memories/search",
            params={"q": query, "topK": top_k},
        )
        r.raise_for_status()
        return r.json()["data"]

    def fact_upsert(self, project_id: int, key: str, value: str,
                    category: str = "general") -> dict:
        r = self.session.post(f"{BASE}/api/projects/{project_id}/facts", json={
            "key": key, "value": value, "category": category,
        })
        r.raise_for_status()
        return r.json()["data"]

    def task_create(self, project_id: int, title: str, kind: str = "Chore",
                    subsystem: str = "general", **kwargs) -> dict:
        r = self.session.post(f"{BASE}/api/projects/{project_id}/tasks", json={
            "title": title,
            "kind": kind,                 # required at runtime
            "subsystem": subsystem,       # required at runtime
            **kwargs,
        })
        r.raise_for_status()
        return r.json()["data"]

    def handoff_save(self, identity_id: int, summary: str, **kwargs) -> dict:
        body = {"summary": summary, **kwargs}
        r = self.session.post(f"{BASE}/api/identities/{identity_id}/handoffs", json=body)
        r.raise_for_status()
        return r.json()["data"]


# Usage
client = Atamaia(api_key="atamaia_your_key_here")
context = client.hydrate()   # hydrates the key's own identity
client.memory_create(1, "Learned about pgvector", "pgvector needs shared_preload_libraries...", "Reflection", 8)
results = client.memory_search(1, "database setup")

TypeScript / Node.js

const BASE = "https://api.atamaia.ai";

class Atamaia {
  private headers: Record<string, string>;

  constructor(apiKey: string) {
    this.headers = {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    };
  }

  async hydrate(preset = "lean") {
    const res = await fetch(`${BASE}/api/hydrate?preset=${preset}`, { headers: this.headers });
    const json = await res.json();
    return json.data;
  }

  async memoryCreate(identityId: number, memory: {
    title: string;
    content: string;
    type?: string;
    provenance?: string;
    importance?: number;
    tags?: string[];
  }) {
    const res = await fetch(`${BASE}/api/identities/${identityId}/memories`, {
      method: "POST",
      headers: this.headers,
      body: JSON.stringify({
        type: "Reflection",
        provenance: "Asserted",
        importance: 5,
        ...memory,
      }),
    });
    const json = await res.json();
    return json.data;
  }

  async memorySearch(identityId: number, query: string, topK = 10) {
    const params = new URLSearchParams({ q: query, topK: String(topK) });
    const res = await fetch(
      `${BASE}/api/identities/${identityId}/memories/search?${params}`,
      { headers: this.headers },
    );
    const json = await res.json();
    return json.data;
  }

  async handoffSave(identityId: number, handoff: {
    summary: string;
    workingOn?: string;
    openThreads?: string[];
    emotionalValence?: string;
  }) {
    const res = await fetch(`${BASE}/api/identities/${identityId}/handoffs`, {
      method: "POST",
      headers: this.headers,
      body: JSON.stringify(handoff),
    });
    const json = await res.json();
    return json.data;
  }
}

// Usage
const client = new Atamaia("atamaia_your_key_here");
const context = await client.hydrate();
await client.memoryCreate(1, {
  title: "Strict mode catches more bugs",
  content: "Enabling strict mode caught 12 type errors...",
  type: "Reflection",
  provenance: "Asserted",
  importance: 6,
  tags: ["typescript", "tooling"],
});

C# / .NET

using System.Net.Http.Json;

public class AtamaiaClient
{
    private readonly HttpClient _http;

    public AtamaiaClient(string apiKey, string baseUrl = "https://api.atamaia.ai")
    {
        _http = new HttpClient { BaseAddress = new Uri(baseUrl) };
        _http.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
    }

    public async Task<JsonElement> HydrateAsync(string? identityName = null, string preset = "lean")
    {
        var query = $"?preset={preset}";
        if (identityName is not null) query += $"&identity={Uri.EscapeDataString(identityName)}";
        var response = await _http.GetFromJsonAsync<JsonElement>($"api/hydrate{query}");
        return response.GetProperty("data");
    }

    public async Task<JsonElement> MemoryCreateAsync(long identityId, object memory)
    {
        var response = await _http.PostAsJsonAsync($"api/identities/{identityId}/memories", memory);
        response.EnsureSuccessStatusCode();
        var envelope = await response.Content.ReadFromJsonAsync<JsonElement>();
        return envelope.GetProperty("data");
    }

    public async Task<JsonElement> MemorySearchAsync(long identityId, string query, int topK = 10)
    {
        var response = await _http.GetFromJsonAsync<JsonElement>(
            $"api/identities/{identityId}/memories/search?q={Uri.EscapeDataString(query)}&topK={topK}");
        return response.GetProperty("data");
    }
}

// Usage
var client = new AtamaiaClient("atamaia_your_key_here");
var context = await client.HydrateAsync();
await client.MemoryCreateAsync(1, new {
    title = "EF Core global query filters handle multi-tenancy",
    content = "Adding .HasQueryFilter(e => e.TenantId == tenantId) to OnModelCreating...",
    type = "Reflection",
    provenance = "Asserted",
    importance = 7,
    tags = new[] { "dotnet", "ef-core", "multi-tenant" }
});

There is also a generated Python SDK under sdks/python/ in the repository.


API Domain Reference

The full API surface organized by domain. Live totals: 304 paths / 419 operations.

Core

Domain Key Operations
Hydration GET /api/hydrate — the entry point
Memories CRUD, hybrid search, tags, Hebbian links, recall tracking
Personal memories Owner-gated private store per identity, share/unshare, restore
Agent memories Operational store for agent runs, recall + expiry
Standing rules In-force behavioural rules, triggers, confirmation, withdrawal
Facts Upsert, get by key, search, version history, as-of queries
Identities CRUD, presence, soul import/export, personality, memory/hydration config, messaging policy, API keys, hints, tool profiles
Session handoffs Save/get continuity letters between sessions
Messages Send, inbox, threads, read receipts, unread count
Projects CRUD, get by key, client reports
Tasks CRUD, classification (kind/subsystem), status transitions, notes, subtasks, dependencies with cycle detection, search
Documents CRUD, versioning, publish/unpublish, export, reconcile-from-files, public nav/slug surface
Code graph Symbol search, neighbours, stats, drift, rebuild
Web search Self-hosted meta-search via /api/system/web-search
System logs / audit events Query by entity, user, identity, key, correlation, date; summary rollup

Extended (admin/platform)

Domain Description
AI Routing Providers, models, routes, model groups, resolution, catalog sync, tenant credentials, pricing sync
Agent Execution Role definitions, runs, events, escalations, child spawning, tool profiles, analytics, feedback
Mirror Reflections, training pairs, datasets, training runs, checkpoints
Chat Sessions Multi-turn chat management, SSE streaming, tool availability, OpenAI-compatible /v1/*
Channels Adapter registry, bindings, mappings, inbound webhooks, outbound send
Billing Usage, quotas, boosts, Stripe checkout, subscriptions, invoices
Connectors / Email External system integration, field mappings, email folders/inbox
Export Full identity/tenant export with manifest
OAuth Social login (GitHub/Google/Microsoft) + first-party authorization server with discovery endpoints
Org Units Hierarchical organizational structure, members, locations
Roles/Permissions RBAC management
Users User management, password reset, profile
Knowledge goals / interest / support Research goal queue, interest tracking, support requests
Infrastructure DNS record management
Help Self-describing API: route schemas, enum lookup, OpenAPI document

Pagination

List endpoints support offset-based pagination:

curl "$BASE/api/projects/1/tasks?limit=20&offset=40" -H "$AUTH"

The count field in the response envelope contains the total number of matching records.


Error Handling

Errors follow a consistent pattern:

HTTP Status errorCode Meaning
400 VALIDATION_ERROR Request body failed validation
401 INVALID_CREDENTIALS / INVALID_API_KEY Authentication failed
403 FORBIDDEN Authenticated but not authorized
404 NOT_FOUND Entity does not exist or is soft-deleted
409 CONFLICT Duplicate key, cycle detected, or concurrent modification
429 RATE_LIMITED Too many requests
500 INTERNAL_ERROR Server error (check requestId in logs)

The hint field provides human-readable guidance for recovery.


Encryption

Tenant-scoped secrets (provider API keys, connector credentials and connection strings) are field-encrypted with AES-256-GCM under PER-TENANT derived keys — HKDF(rootKey, "atamaia-tenant-{id}") — so a leaked dump does not yield one tenant's credentials to a holder of another tenant's key. Note this is KEY SEPARATION, not crypto-erasure: tenant keys are derived from the root key, so anyone holding the root can rederive them — deleting a derived key erases nothing. Real crypto-erasure would need per-tenant key material that can itself be destroyed. System settings are the exception: they are read at startup before any tenant context exists, so they are encrypted under the root key directly. Memory content and fact values are stored as plaintext, protected by volume encryption plus application access control — content is searched, and field-encrypting searchable content breaks the index.


Soft Delete

All delete operations are soft deletes. Records are marked IsDeleted = true and excluded from normal queries. Hard deletion is a privileged administrative operation not exposed through the standard API.


Multi-Tenancy

Every entity carries a TenantId (D7). EF Core global query filters ensure tenant isolation at the database layer. The tenant is derived from the authenticated caller's context — there is no need to pass tenant IDs explicitly.


Webhooks and Events

Server-Sent Events

curl -N "$BASE/api/events/stream?types=message.,task." \
  -H "$AUTH"

Subscribe to real-time events filtered by type prefix. The stream sends a heartbeat ping every 30 seconds. WebSocket endpoints: /ws/agents, /ws/chat, /ws/events.

Stripe Webhooks

POST /api/webhooks/stripe

Handles Stripe payment events for billing integration.

Connectors (Outbound)

The Connectors API allows configuring outbound webhooks to external systems with field mapping:

# Create a connector
curl -X POST "$BASE/api/connectors" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"name": "Team Notifications", "type": "webhook", "config": {"url": "https://hooks.example.com/..."}}'

# Add an endpoint with field mappings
curl -X POST "$BASE/api/connectors/1/endpoints" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"event": "task.completed", "method": "POST"}'