/**
* Attio API Client
* Wrapper for Attio CRM REST API
* https://developers.attio.com/reference
*/
/**
* Attio API client configuration
*/
export interface AttioClientConfig {
apiKey: string;
baseUrl?: string;
timeout?: number;
retryAttempts?: number;
retryDelay?: number;
}
/**
* HTTP request options
*/
interface RequestOptions {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
path: string;
body?: unknown;
params?: Record<string, string | number | boolean>;
}
/**
* Attio API error
*/
export class AttioError extends Error {
constructor(
message: string,
public statusCode?: number,
public response?: unknown
) {
super(message);
this.name = 'AttioError';
}
}
/**
* Attio API Client
*/
export class AttioClient {
private readonly apiKey: string;
private readonly baseUrl: string;
private readonly timeout: number;
private readonly retryAttempts: number;
private readonly retryDelay: number;
constructor(config: AttioClientConfig) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl || 'https://api.attio.com/v2';
this.timeout = config.timeout || 30000; // 30 seconds
this.retryAttempts = config.retryAttempts || 3;
this.retryDelay = config.retryDelay || 1000; // 1 second
}
/**
* Make an HTTP request to the Attio API
*/
private async request<T>(options: RequestOptions): Promise<T> {
const { method, path, body, params } = options;
// Build URL with query parameters
const url = new URL(`${this.baseUrl}${path}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
}
// Build request options
const fetchOptions: RequestInit = {
method,
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
signal: AbortSignal.timeout(this.timeout),
};
if (body) {
fetchOptions.body = JSON.stringify(body);
}
// Execute request with retry logic
let lastError: Error | undefined;
for (let attempt = 0; attempt < this.retryAttempts; attempt++) {
try {
if (attempt > 0) {
// Wait before retry (exponential backoff)
await this.sleep(this.retryDelay * Math.pow(2, attempt - 1));
console.error(
`Retrying Attio API request (attempt ${attempt + 1}/${this.retryAttempts})...`
);
}
const response = await fetch(url.toString(), fetchOptions);
// Handle non-2xx responses
if (!response.ok) {
const errorBody = await response.text();
let errorMessage = `Attio API error: ${response.status} ${response.statusText}`;
try {
const errorJson = JSON.parse(errorBody);
errorMessage = errorJson.message || errorMessage;
} catch {
// Error body is not JSON, use status text
}
// Don't retry client errors (4xx)
if (response.status >= 400 && response.status < 500) {
throw new AttioError(errorMessage, response.status, errorBody);
}
// Retry server errors (5xx)
lastError = new AttioError(errorMessage, response.status, errorBody);
continue;
}
// Parse response
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
return (await response.json()) as T;
} else {
// Return empty object for non-JSON responses (e.g., 204 No Content)
return {} as T;
}
} catch (error) {
if (error instanceof AttioError) {
throw error;
}
// Network errors, timeouts, etc.
lastError = error instanceof Error ? error : new Error(String(error));
// Don't retry if we've exhausted attempts
if (attempt === this.retryAttempts - 1) {
break;
}
}
}
// All retries failed
throw new AttioError(
`Attio API request failed after ${this.retryAttempts} attempts: ${lastError?.message}`,
undefined,
lastError
);
}
/**
* Sleep utility for retry delays
*/
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* GET request
*/
async get<T>(
path: string,
params?: Record<string, string | number | boolean>
): Promise<T> {
return this.request<T>({ method: 'GET', path, params });
}
/**
* POST request
*/
async post<T>(path: string, body: unknown): Promise<T> {
return this.request<T>({ method: 'POST', path, body });
}
/**
* PUT request
*/
async put<T>(path: string, body: unknown): Promise<T> {
return this.request<T>({ method: 'PUT', path, body });
}
/**
* PATCH request
*/
async patch<T>(path: string, body: unknown): Promise<T> {
return this.request<T>({ method: 'PATCH', path, body });
}
}
/**
* Create an Attio client instance
*/
export function createAttioClient(apiKey: string): AttioClient {
return new AttioClient({ apiKey });
}