open-wa
DocsExternal API Patterns

External API Patterns

Handle HTTP responses, retries, timeouts, and concurrency in plugins.

External API Patterns

Use this guide when a plugin calls an HTTP service, such as an LLM or CRM. It covers response handling, timeouts, retries, and bounded concurrency.

Making HTTP Requests

Use 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 axios

Error 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. Add a retry queue if it must be retried.
  }

  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 });
}

Timeout a request

try {
  const response = await fetch(url, { signal: AbortSignal.timeout(10000) });
  // Process response
} catch (error) {
  if (error.name === 'TimeoutError' || 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;
        if (attempt === maxRetries) return response;
        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));
    }
  }
}

Choose how the handler waits

Handle work asynchronously

An awaited network request keeps this handler pending. If it should return before the request completes, start the work separately and handle its rejection:

// Waits for the request before this hook completes.
events.on('message.received', async ({ message }) => {
  const result = await slowAPI(message.body);
  await client.sendText(message.from, result);
});

// Returns without waiting for the request; rejection is logged.
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 }) => {
  await queue.add(async () => {
    const result = await callAPI(message.body);
    await client.sendText(message.from, result);
  }).catch(error => {
    logger.error('Queued request failed', { error: error.message });
  });
});

Bound how long you wait

Pass a deadline to the external client when it supports cancellation. AbortSignal.timeout() stops waiting after five seconds; the service must honor the signal to cancel the request:

events.on('message.received', async ({ message }) => {
  try {
    const result = await callAPI(message.body, { signal: AbortSignal.timeout(5000) });
    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
});

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:

// Remove expired entries once a minute.
setInterval(() => {
  const now = Date.now();
  for (const [key, entry] of cache.entries()) {
    if (now - entry.timestamp > CACHE_TTL) {
      cache.delete(key);
    }
  }
}, 60 * 1000);

Was this helpful?

Your answer includes the page path and docs version.

On this page