Back to The Times of Claw

Building a Dench App That Calls External APIs

Learn how to build a DenchClaw app that calls external APIs using dench.http.fetch(), handle authentication, manage rate limits, and store API keys securely.

Mark Rachapoom
Mark Rachapoom
·5 min read
Building a Dench App That Calls External APIs

Building a Dench App That Calls External APIs

DenchClaw runs locally, which means your apps are subject to the same browser restrictions as any web app: CORS blocks most external API calls. The dench.http.fetch() bridge API solves this — it proxies requests through the DenchClaw backend, which runs server-side without CORS restrictions.

This guide covers everything about building apps that call external APIs: the http:external permission, authentication patterns, rate limiting, error handling, and secure key storage.

The dench.http.fetch() API#

The interface mirrors the standard fetch() API but runs server-side:

const response = await dench.http.fetch(url, options);

Parameters:

  • url — The target URL (must be HTTPS for external calls)
  • options.method — HTTP method (GET, POST, PUT, DELETE)
  • options.headers — Request headers object
  • options.body — Request body (string or JSON-serializable object)
  • options.timeout — Request timeout in milliseconds (default: 30000)

Returns: A response object with status, headers, and json() / text() methods.

Required permission: http:external in .dench.yaml.

Pattern 1: Simple GET Request#

// Weather API example (no authentication required)
const weather = await dench.http.fetch(
  'https://wttr.in/San+Francisco?format=j1',
  { method: 'GET' }
);
const data = await weather.json();
console.log(data.current_condition[0].temp_C);

Pattern 2: Authenticated API (Bearer Token)#

Most production APIs require authentication. Store the token in dench.store rather than hardcoding it:

async function fetchWithAuth(url, options = {}) {
  const apiKey = await dench.store.get('my_api_key');
  if (!apiKey) {
    throw new Error('API key not configured. Set it in app settings.');
  }
  
  return dench.http.fetch(url, {
    ...options,
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
      ...options.headers
    }
  });
}
 
// Usage
const response = await fetchWithAuth('https://api.example.com/v1/companies');
const data = await response.json();

Pattern 3: API Key in Settings UI#

Build a settings panel so users can enter their API keys:

async function checkApiKey() {
  const key = await dench.store.get('apollo_api_key');
  if (!key) {
    document.getElementById('app').innerHTML = `
      <div class="settings-prompt">
        <h2>Configure API Key</h2>
        <p>This app requires an Apollo.io API key.</p>
        <input type="password" id="apiKeyInput" placeholder="Enter your API key...">
        <button onclick="saveApiKey()">Save</button>
        <a href="https://app.apollo.io/#/settings/integrations/api" target="_blank">Get API key →</a>
      </div>
    `;
    return false;
  }
  return true;
}
 
async function saveApiKey() {
  const key = document.getElementById('apiKeyInput').value;
  if (!key) return;
  await dench.store.set('apollo_api_key', key);
  await dench.ui.toast({ message: 'API key saved', type: 'success' });
  location.reload();
}

Pattern 4: Rate Limiting#

Most APIs have rate limits. Implement a simple rate limiter:

class RateLimiter {
  constructor(requestsPerSecond) {
    this.interval = 1000 / requestsPerSecond;
    this.lastCall = 0;
  }
  
  async wait() {
    const now = Date.now();
    const timeSinceLast = now - this.lastCall;
    if (timeSinceLast < this.interval) {
      await new Promise(r => setTimeout(r, this.interval - timeSinceLast));
    }
    this.lastCall = Date.now();
  }
}
 
const limiter = new RateLimiter(2); // 2 requests per second
 
async function enrichBatch(contacts) {
  const results = [];
  for (const contact of contacts) {
    await limiter.wait();
    const result = await enrichContact(contact);
    results.push(result);
  }
  return results;
}

Pattern 5: Retry on Failure with Exponential Backoff#

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  let lastError;
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await dench.http.fetch(url, options);
      
      if (response.status === 429) {
        // Rate limited — wait and retry
        const retryAfter = Number(response.headers['retry-after'] || (2 ** attempt));
        await new Promise(r => setTimeout(r, retryAfter * 1000));
        continue;
      }
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${await response.text()}`);
      }
      
      return response;
    } catch (err) {
      lastError = err;
      if (attempt < maxRetries - 1) {
        await new Promise(r => setTimeout(r, 2 ** attempt * 1000)); // Exponential backoff
      }
    }
  }
  throw lastError;
}

Real-World Example: Apollo.io Lead Enrichment#

Here's a complete example integrating with Apollo.io's People API:

async function enrichWithApollo(contact) {
  const apiKey = await dench.store.get('apollo_api_key');
  
  const response = await dench.http.fetch(
    'https://api.apollo.io/api/v1/people/match',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': 'no-cache'
      },
      body: JSON.stringify({
        api_key: apiKey,
        first_name: contact['Full Name']?.split(' ')[0],
        last_name: contact['Full Name']?.split(' ').slice(1).join(' '),
        organization_name: contact.Company,
        email: contact['Email Address']
      })
    }
  );
  
  if (!response.ok) {
    throw new Error(`Apollo API error: ${response.status}`);
  }
  
  const data = await response.json();
  const person = data.person;
  
  if (!person) return null;
  
  return {
    linkedin_url: person.linkedin_url,
    title: person.title,
    company_size: person.organization?.estimated_num_employees,
    company_domain: person.organization?.primary_domain,
    phone: person.phone_numbers?.[0]?.raw_number
  };
}

Handling Common Error Cases#

async function safeExternalCall(url, options) {
  try {
    const response = await dench.http.fetch(url, { ...options, timeout: 10000 });
    
    switch (response.status) {
      case 200:
      case 201:
        return await response.json();
      case 401:
        await dench.store.delete('api_key'); // Clear invalid key
        throw new Error('Invalid API key. Please reconfigure.');
      case 403:
        throw new Error('Access denied. Check your API plan limits.');
      case 404:
        return null; // Not found is not an error
      case 429:
        throw new Error('Rate limit exceeded. Wait before retrying.');
      case 500:
      case 502:
      case 503:
        throw new Error('API service temporarily unavailable.');
      default:
        throw new Error(`Unexpected status: ${response.status}`);
    }
  } catch (err) {
    if (err.name === 'TimeoutError') {
      throw new Error('Request timed out. The API may be slow.');
    }
    throw err;
  }
}

Frequently Asked Questions#

Does dench.http.fetch() support file uploads?#

File upload support (multipart/form-data) is planned. Currently, the bridge handles JSON and text bodies. For APIs requiring file upload, use base64 encoding in a JSON body if the API supports it.

Can I call HTTP (non-HTTPS) endpoints?#

For security, external calls are restricted to HTTPS. Local development APIs running on localhost or 127.0.0.1 are accessible without HTTPS.

How do I debug failing API calls?#

Add console.log before and after the call. Check the DenchClaw backend logs for network-level errors. The bridge will log requests and responses at the debug level.

Is there a request size limit?#

Request bodies are limited to 10MB. Response bodies are limited to 50MB. For larger payloads, consider streaming or pagination.

Can I use WebSockets through the bridge?#

Not currently. dench.http.fetch() is request/response only. For real-time external data, poll the API using dench.cron.schedule() or the widget refreshMs mechanism.

Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →

Mark Rachapoom

Written by

Mark Rachapoom

Building the future of AI CRM software.

Continue reading

DENCH

© 2026 DenchHQ · San Francisco, CA