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
curlon 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/stripeHandles 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"}'