Loading...
Loading...

An AI agent without API integrations is just a chatbot. It can talk. It can't do.
The moment your agent can create a Jira ticket, send a Slack message, update a spreadsheet, or charge a credit card -- that's when it becomes useful. That's when it replaces actual work instead of just describing work.
But API integrations are where most agent projects die. Rate limits. Authentication nightmares. Error handling. Versioning. Timeouts.
Here are the patterns I use to make integrations reliable.
Never let your AI agent call APIs directly. Put an adapter between them.
// BAD: AI tool calls Stripe directly
async function createSubscription(customerId: string, priceId: string) {
return await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
});
}
// GOOD: Adapter with validation, logging, and error handling
class StripeAdapter {
async createSubscription(params: {
customerId: string;
priceId: string;
}): Promise<AdapterResult> {
// Validate before calling
if (!params.customerId.startsWith("cus_")) {
return { success: false, error: "Invalid customer ID format" };
}
try {
const subscription = await stripe.subscriptions.create({
customer: params.customerId,
items: [{ price: params.priceId }],
});
// Log for auditing
await this.log("subscription_created", {
customerId: params.customerId,
subscriptionId: subscription.id,
});
return {
success: true,
data: {
subscriptionId: subscription.id,
status: subscription.status,
currentPeriodEnd: subscription.current_period_end,
},
};
} catch (error: any) {
return {
success: false,
error: this.humanizeError(error),
retryable: error.statusCode === 429 || error.statusCode >= 500,
};
}
}
private humanizeError(error: any): string {
// Convert API errors to messages the AI can understand
const errorMap: Record<string, string> = {
card_declined: "The customer's card was declined",
insufficient_funds: "The customer has insufficient funds",
invalid_customer: "The customer ID doesn't exist in Stripe",
};
return errorMap[error.code] || `Stripe error: \${error.message}`;
}
}The adapter does three things the direct call doesn't:
The AI doesn't need to know Stripe's response format. It needs to know: did it work? If not, why? Can I retry?
Every API has rate limits. Your agent will hit them because agents are fast and persistent.
class RateLimitedClient {
private queue: Array<{
fn: () => Promise<any>;
resolve: (value: any) => void;
reject: (error: any) => void;
}> = [];
private processing = false;
private requestsThisWindow = 0;
private windowStart = Date.now();
private maxPerWindow: number;
private windowMs: number;
constructor(maxPerWindow: number, windowMs: number = 60000) {
this.maxPerWindow = maxPerWindow;
this.windowMs = windowMs;
}
async execute<T>(fn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this.processQueue();
});
}
private async processQueue() {
if (this.processing) return;
this.processing = true;
while (this.queue.length > 0) {
// Reset window if needed
if (Date.now() - this.windowStart > this.windowMs) {
this.requestsThisWindow = 0;
this.windowStart = Date.now();
}
// Wait if at limit
if (this.requestsThisWindow >= this.maxPerWindow) {
const waitTime = this.windowMs - (Date.now() - this.windowStart);
await new Promise((r) => setTimeout(r, waitTime + 100));
this.requestsThisWindow = 0;
this.windowStart = Date.now();
}
const item = this.queue.shift()!;
try {
this.requestsThisWindow++;
const result = await item.fn();
item.resolve(result);
} catch (error) {
item.reject(error);
}
}
this.processing = false;
}
}
// Usage
const githubClient = new RateLimitedClient(30, 60000); // 30 req/min
async function createIssue(title: string, body: string) {
return githubClient.execute(() =>
octokit.issues.create({
owner: "myorg",
repo: "myrepo",
title,
body,
})
);
}The agent doesn't know about rate limits. It just calls the function. The rate limiter handles the rest.
Some API calls fail transiently. Network blip. Server hiccup. The solution is always the same: wait and try again.
async function withRetry<T>(
fn: () => Promise<T>,
options: {
maxRetries?: number;
baseDelayMs?: number;
maxDelayMs?: number;
retryableErrors?: number[];
} = {}
): Promise<T> {
const {
maxRetries = 3,
baseDelayMs = 1000,
maxDelayMs = 30000,
retryableErrors = [429, 500, 502, 503, 504],
} = options;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
const isLastAttempt = attempt === maxRetries;
const isRetryable =
retryableErrors.includes(error.status || error.statusCode);
if (isLastAttempt || !isRetryable) throw error;
const delay = Math.min(
baseDelayMs * Math.pow(2, attempt) + Math.random() * 1000,
maxDelayMs
);
console.log(
`Retry \${attempt + 1}/\${maxRetries} after \${Math.round(delay)}ms`
);
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error("Unreachable");
}Exponential backoff with jitter. The jitter (random component) prevents thundering herd when multiple agents retry simultaneously.
When a service is down, stop hitting it. Failing fast is better than timing out.
class CircuitBreaker {
private failures = 0;
private lastFailure = 0;
private state: "closed" | "open" | "half-open" = "closed";
private threshold: number;
private resetTimeMs: number;
constructor(threshold: number = 5, resetTimeMs: number = 60000) {
this.threshold = threshold;
this.resetTimeMs = resetTimeMs;
}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === "open") {
if (Date.now() - this.lastFailure > this.resetTimeMs) {
this.state = "half-open";
} else {
throw new Error(
"Circuit breaker is open. Service is temporarily unavailable."
);
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = "closed";
}
private onFailure() {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.threshold) {
this.state = "open";
}
}
}Five failures in a row? Stop trying for a minute. Try one more time. If it works, resume. If not, wait another minute.
The AI gets a clear error message: "Service temporarily unavailable." It can tell the user, try an alternative, or wait.
When your agent has many integrations, organize them.
interface Tool {
name: string;
description: string;
parameters: Record<string, any>;
execute: (params: any) => Promise<AdapterResult>;
rateLimit?: { max: number; windowMs: number };
requiresConfirmation?: boolean;
}
class ToolRegistry {
private tools: Map<string, Tool> = new Map();
private rateLimiters: Map<string, RateLimitedClient> = new Map();
private circuitBreakers: Map<string, CircuitBreaker> = new Map();
register(tool: Tool) {
this.tools.set(tool.name, tool);
if (tool.rateLimit) {
this.rateLimiters.set(
tool.name,
new RateLimitedClient(tool.rateLimit.max, tool.rateLimit.windowMs)
);
}
this.circuitBreakers.set(tool.name, new CircuitBreaker());
}
async execute(
toolName: string,
params: any
): Promise<AdapterResult> {
const tool = this.tools.get(toolName);
if (!tool) return { success: false, error: `Unknown tool: \${toolName}` };
const breaker = this.circuitBreakers.get(toolName)!;
const limiter = this.rateLimiters.get(toolName);
const fn = () => breaker.execute(() => tool.execute(params));
return limiter ? limiter.execute(fn) : fn();
}
getToolDefinitions() {
return Array.from(this.tools.values()).map((t) => ({
name: t.name,
description: t.description,
input_schema: t.parameters,
}));
}
}Register your tools once. Rate limiting, circuit breaking, and error handling apply automatically. The AI gets clean tool definitions. The registry handles the chaos.
Some actions shouldn't happen without human approval.
// Mark certain tools as requiring confirmation
registry.register({
name: "delete_customer",
description: "Permanently delete a customer account",
parameters: { ... },
execute: deleteCustomer,
requiresConfirmation: true // Flag this
});
// In your agent loop, check before executing
if (tool.requiresConfirmation) {
return {
type: "confirmation_needed",
action: toolName,
params: params,
message: `The agent wants to delete customer \${params.customerId}. Approve?`
};
}The agent suggests the action. A human approves it. The agent executes it.
For billing, data deletion, and anything else where a mistake costs money or trust, this pattern is non-negotiable.
Your integrations are the hands and feet of your AI agent. Build them with the same care you'd build any production system.
Validate inputs. Handle errors gracefully. Respect rate limits. Fail fast when services are down. Require confirmation for destructive actions.
The AI is smart. Your integrations need to be reliable.
Those are different skills. Don't confuse them.

Understanding the Model Context Protocol — how it standardizes tool use, enables agent interoperability, and creates a universal integration layer.

Design, implement, test, and document REST and GraphQL APIs using AI agents that understand best practices and generate production-ready endpoints.

RAG is how you make AI actually know things about your business. Not fine-tuning. Not prompt engineering. Retrieval. Here's the complete build.
Stop reading about AI and start building with it. Book a free discovery call and see how AI agents can accelerate your business.