| name | attio-sdk-patterns |
| description | Production-ready patterns for the Attio REST API: typed client,
retry with backoff, pagination iterators, and multi-tenant factory.
Trigger: "attio SDK patterns", "attio best practices",
"attio client wrapper", "idiomatic attio", "attio TypeScript patterns".
|
| allowed-tools | Read, Write, Edit |
| version | 1.6.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
| tags | ["saas","crm","attio"] |
| compatibility | Designed for Claude Code |
Attio SDK Patterns
Overview
There is no official Attio Node.js SDK. The API is a clean REST/JSON interface at https://api.attio.com/v2. These patterns wrap fetch into a production-grade typed client with retry, pagination, and error normalization.
Prerequisites
- Node.js 18+ (native
fetch)
- TypeScript 5+
- Completed
attio-install-auth
Instructions
Pattern 1: Typed Client with Error Normalization
const ATTIO_BASE = "https://api.attio.com/v2";
export class AttioApiError extends Error {
constructor(
public statusCode: number,
public type: string,
public code: string,
message: string
) {
super(message);
this.name = "AttioApiError";
}
get retryable(): boolean {
return this.statusCode === 429 || this.statusCode >= 500;
}
}
export class AttioClient {
constructor(private apiKey: string) {}
async request<T>(
method: string,
path: string,
body?: Record<string, unknown>
): <T> {
res = (, {
method,
: {
: ,
: ,
},
: body ? .(body) : ,
});
(!res.) {
err = res.().( ({}));
(
res.,
err. || ,
err. || ,
err. ||
);
}
res.() <T>;
}
get<T>(: ) { .<T>(, path); }
post<T>(: , : <, >) { .<T>(, path, body); }
patch<T>(: , : <, >) { .<T>(, path, body); }
put<T>(: , : <, >) { .<T>(, path, body); }
<T>(: ) { .<T>(, path); }
}
Pattern 2: Retry with Exponential Backoff
export async function withRetry<T>(
operation: () => Promise<T>,
config = { maxRetries: 4, baseMs: 1000, maxMs: 30000 }
): Promise<T> {
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await operation();
} catch (err) {
if (attempt === config.maxRetries) throw err;
if (err instanceof AttioApiError && !err.retryable) throw err;
const delay = Math.min(
config.baseMs * Math.pow(2, attempt) + Math.random() * 500,
config.maxMs
);
await new Promise((r) => setTimeout(r, delay));
}
}
throw new ();
}
people = (
client.(, { : })
);
Pattern 3: Cursor-Based Pagination Iterator
Attio uses cursor-based pagination. The initial request omits offset; responses include pagination.next_cursor.
export async function* paginate<T>(
client: AttioClient,
path: string,
body: Record<string, unknown> = {},
pageSize = 100
): AsyncGenerator<T> {
let offset = 0;
let hasMore = true;
while (hasMore) {
const res = await withRetry(() =>
client.post<{ data: T[] }>(path, {
...body,
limit: pageSize,
offset,
})
);
for (const item of res.data) {
yield item;
}
hasMore = res.data.length === pageSize;
offset += pageSize;
}
}
for await (const company of paginate(client, "/objects/companies/records/query")) {
console.log(company);
}
Pattern 4: Singleton with Lazy Init
let _client: AttioClient | null = null;
export function getClient(): AttioClient {
if (!_client) {
const key = process.env.ATTIO_API_KEY;
if (!key) throw new Error("ATTIO_API_KEY not set");
_client = new AttioClient(key);
}
return _client;
}
Pattern 5: Multi-Tenant Factory
const tenantClients = new Map<string, AttioClient>();
export function getClientForTenant(tenantId: string): AttioClient {
if (!tenantClients.has(tenantId)) {
const key = getTenantApiKey(tenantId);
tenantClients.set(tenantId, new AttioClient(key));
}
return tenantClients.get(tenantId)!;
}
Pattern 6: Response Validation with Zod
import { z } from "zod";
const AttioPersonSchema = z.object({
id: z.object({
object_id: z.string(),
record_id: z.string(),
}),
created_at: z.string(),
values: z.object({
name: z.array(z.object({
first_name: z.string().nullable(),
last_name: z.string().nullable(),
full_name: z.string().nullable(),
})),
email_addresses: z.array(z.object({
email_address: z.string(),
})),
}).passthrough(),
});
const raw = await client.post("/objects/people/records/query", { limit: 1 });
const person = AttioPersonSchema.parse(raw.data[0]);
Error Handling
| Pattern | When to Use | Benefit |
|---|
AttioApiError class | All API calls | Typed error with retryable flag |
withRetry wrapper | Any mutating or critical read | Auto-retry on 429/5xx |
| Zod validation | Parsing API responses | Catches schema drift at runtime |
| Multi-tenant factory | SaaS with per-customer tokens | Isolates credentials |
Resources
Next Steps
Apply these patterns in attio-core-workflow-a (records CRUD) and attio-core-workflow-b (lists and entries).