External API Patterns
Best practices for calling external services from plugins without blocking the message pipeline.

External API Patterns
Plugins often need to call external services: LLMs, CRMs, databases, or third-party APIs. This guide covers how to do that reliably without blocking the WhatsApp message pipeline.
Making HTTP Requests
Using fetch
Node.js 22+ includes native fetch:
events.on('message.received', async ({ message }) => {
const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`,
},
body: JSON.stringify({ query: message.body }),
});
const result = await response.json();
});Adding Peer Dependencies
If you need axios or other HTTP libraries, add them as peer dependencies:
{
"peerDependencies": {
"axios": ">=1.0.0"
}
}Users install them separately:
npm install axiosError Handling
HTTP Status Codes
try {
const response = await fetch(url);
if (response.status === 429) {
// Rate limited
const retryAfter = response.headers.get('Retry-After');
logger.warn('Rate limited', { retryAfter });
return; // Skip this message, retry later
}
if (response.status === 401) {
logger.error('Invalid API key');
return;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
} catch (error) {
logger.error('API call failed', { error: error.message });
}Timeouts
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10 seconds
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeout);
// Process response
} catch (error) {
if (error.name === 'AbortError') {
logger.warn('Request timed out');
} else {
logger.error('Request failed', { error: error.message });
}
}Retry with Exponential Backoff
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000;
logger.warn('Rate limited, retrying', { attempt, delay });
await new Promise(r => setTimeout(r, delay));
continue;
}
return response; // Non-429 error, return it
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.pow(2, attempt) * 1000;
logger.warn('Retrying', { attempt, delay });
await new Promise(r => setTimeout(r, delay));
}
}
}Not Blocking the Pipeline
Async Processing
Never block the message handler with slow operations:
// BAD - blocks the pipeline
events.on('message.received', async ({ message }) => {
const result = await slowAPI(message.body); // Blocks all other messages
await client.sendText(message.from, result);
});
// GOOD - fire and forget
events.on('message.received', async ({ message }) => {
processMessage(message).catch(error => {
logger.error('Processing failed', { error: error.message });
});
});Queue-Based Processing
Use a queue to control concurrency:
import pQueue from 'p-queue';
const queue = new pQueue({ concurrency: 3 });
events.on('message.received', async ({ message }) => {
queue.add(async () => {
const result = await callAPI(message.body);
await client.sendText(message.from, result);
});
});Timeout-Based Skipping
If an API call takes too long, skip it rather than blocking:
events.on('message.received', async ({ message }) => {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000)
);
try {
const result = await Promise.race([
callAPI(message.body),
timeout,
]);
await client.sendText(message.from, result);
} catch (error) {
logger.warn('API call skipped', { reason: error.message });
}
});Performance
Concurrent Call Limits
Set reasonable concurrency limits:
const queue = new pQueue({
concurrency: 5, // Maximum 5 concurrent API calls
timeout: 30000, // 30 second timeout per task
});Caching
Cache API responses for identical requests:
const cache = new Map<string, { data: unknown; timestamp: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async function getCached(key: string) {
const entry = cache.get(key);
if (entry && Date.now() - entry.timestamp < CACHE_TTL) {
return entry.data;
}
return null;
}
async function setCached(key: string, data: unknown) {
cache.set(key, { data, timestamp: Date.now() });
}Memory Management
Clear caches periodically:
// Clear cache every hour
setInterval(() => {
const now = Date.now();
for (const [key, entry] of cache.entries()) {
if (now - entry.timestamp > CACHE_TTL) {
cache.delete(key);
}
}
}, 60 * 60 * 1000);Batch Processing
Batch multiple requests when possible:
let batch: Array<{ message: unknown; resolve: (value: unknown) => void; reject: (error: Error) => void }> = [];
let batchTimer: NodeJS.Timeout | null = null;
function enqueue(message: unknown): Promise<unknown> {
return new Promise((resolve, reject) => {
batch.push({ message, resolve, reject });
if (!batchTimer) {
batchTimer = setTimeout(() => {
processBatch(batch);
batch = [];
batchTimer = null;
}, 1000); // Process every 1 second
}
});
}Related
- Plugin getting started - Build your first plugin
- Plugin security model - Security boundaries
- Hooks reference - Available hooks

Was this helpful?
Wally and his cute companion coffee mug are coding day and night to keep this up-to-date!
