open-wa v5 is alpha. Use v4.76.0 for mature production systems unless you are validating v5.
The Client APIAPI ExplorerLicensing

External API Patterns

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

Plugin Api Bridge Wally

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 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, 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
    }
  });
}
Wally the Walrus typing

Was this helpful?

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

On this page