How to Add Company Intelligence to Your Chrome Extension (with BounceWatch API)

Api Data ·
Bounce Watch Bounce Watch Team
· · 22 min read · 113 views
How to Add Company Intelligence to Your Chrome Extension (with BounceWatch API)

You're browsing a company's website and want instant intel: funding stage, employee count, recent signals, growth trajectory. You could open a new tab, search for the company, dig through multiple databases, and piece together a profile manually. Or you could build a Chrome extension powered by a company data API that makes this happen in one click. One click, and a compact intelligence card slides into view with everything you need to know.

In this tutorial, we'll build exactly that. A Chrome extension that detects the domain you're visiting, fetches real-time company data and growth signals from the BounceWatch API, and displays an actionable intelligence card right in your browser. Whether you're building a sales chrome extension, a prospecting tool, or a competitive intelligence sidebar, this guide gives you the complete blueprint with working code.

By the end, you'll have a fully functional browser extension for company data that your sales team, investors, or analysts can use every day. Let's build it.

What We're Building

The finished extension is a compact sidebar that activates when you click the extension icon on any website. Here's what it does:

  • Detects the current domain — automatically extracts the company domain from the tab you're viewing
  • Fetches company data via API — pulls structured company intelligence from BounceWatch in real time
  • Displays a compact intelligence card — shows company name, industry, employee count, funding stage, total funding, country, and founded year in a clean, scannable layout
  • Shows signal badges — visual indicators like "Recently Funded," "Hiring Surge," and "Expanding" so you instantly know what's happening with the company
  • Provides a signal timeline — a feed of recent company events and growth signals
  • Offers quick actions — track the company, open it in BounceWatch, or copy a summary to your clipboard

Picture this: you land on a SaaS company's homepage during research. You click the extension icon. Within a second, a card appears showing "Series B, 230 employees, $42M total funding, Hiring Surge detected 3 days ago." That's the power of combining a chrome extension company lookup API with a clean user interface.

Architecture Overview

Chrome extensions built on Manifest V3 follow a clear separation of concerns. Here's how data flows through our extension:

manifest.json (configuration + permissions)
       │
       ├── popup.html (UI layer — the intelligence card)
       │        │
       │        └── popup.js (render logic + event handlers)
       │
       ├── background.js (service worker — API calls + caching)
       │        │
       │        └── BounceWatch API (external data source)
       │
       └── content.js (optional — domain detection from page context)

The flow is straightforward:

  1. User clicks the extension icon on any website
  2. popup.js asks the background service worker for the current tab's URL
  3. background.js extracts the domain, checks the local cache, and if needed, calls the BounceWatch API
  4. The API returns structured company data and signals
  5. popup.js renders the intelligence card with the response data

This architecture keeps API keys safe in the service worker (never exposed to page content), allows for efficient caching, and follows Chrome's Manifest V3 best practices. Let's start building.

Step 1 — Set Up the Chrome Extension

Create a new directory for your extension project. We'll need four files to start: the manifest, popup HTML, popup script, and the background service worker.

manifest.json

The manifest file defines your extension's configuration, permissions, and entry points. We're using Manifest V3, which is now required for all new Chrome extensions.

{
  "manifest_version": 3,
  "name": "Company Intelligence by BounceWatch",
  "version": "1.0.0",
  "description": "Instant company data, funding info, and growth signals for any website you visit.",
  "permissions": [
    "activeTab",
    "storage"
  ],
  "host_permissions": [
    "https://www.bouncewatch.com/*"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon-16.png",
      "48": "icons/icon-48.png",
      "128": "icons/icon-128.png"
    }
  },
  "background": {
    "service_worker": "background.js"
  },
  "icons": {
    "16": "icons/icon-16.png",
    "48": "icons/icon-48.png",
    "128": "icons/icon-128.png"
  }
}

A few things to note about the permissions:

  • activeTab — lets us read the URL of the tab the user is currently viewing, but only when they click the extension icon. This is the least-privilege approach and doesn't require broad tab access.
  • storage — allows us to cache API responses and store the user's API key locally using chrome.storage.local.
  • host_permissions — grants permission to make API calls to the BounceWatch domain from the service worker.

Basic Popup HTML

Create popup.html as the skeleton for our intelligence card. We'll keep it minimal for now and add styling later.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Company Intelligence</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div id="app">
    <div id="loading" class="state">
      <div class="spinner"></div>
      <p>Looking up company...</p>
    </div>
    <div id="company-card" class="state hidden"></div>
    <div id="error" class="state hidden"></div>
    <div id="no-api-key" class="state hidden">
      <h3>API Key Required</h3>
      <input type="text" id="api-key-input" placeholder="Enter your BounceWatch API key">
      <button id="save-key-btn">Save Key</button>
    </div>
  </div>
  <script src="popup.js"></script>
</body>
</html>

The popup uses state-based rendering: only one .state div is visible at a time (loading, company card, error, or API key prompt). This pattern keeps the UI clean and predictable.

Step 2 — Detect the Current Domain

The first real logic our extension needs is domain detection. When the user clicks the extension icon, we need to figure out which company's website they're on.

Getting the Active Tab URL

In the popup script, we use the chrome.tabs API to query the active tab. Since we declared the activeTab permission, this works without requesting broader access.

// popup.js — Domain detection

async function getCurrentDomain() {
  const [tab] = await chrome.tabs.query({
    active: true,
    currentWindow: true
  });

  if (!tab?.url) {
    throw new Error('Cannot access this tab');
  }

  return extractDomain(tab.url);
}

function extractDomain(url) {
  try {
    const urlObj = new URL(url);
    let hostname = urlObj.hostname;

    // Strip 'www.' prefix
    hostname = hostname.replace(/^www\./, '');

    // Skip non-company domains
    const skipDomains = [
      'google.com', 'bing.com', 'duckduckgo.com',
      'github.com', 'stackoverflow.com', 'reddit.com',
      'twitter.com', 'x.com', 'facebook.com',
      'linkedin.com', 'youtube.com', 'amazon.com',
      'chrome:', 'about:', 'newtab'
    ];

    if (skipDomains.some(d => hostname.includes(d))) {
      throw new Error('This domain is not a company website');
    }

    // Skip browser internal pages
    if (!urlObj.protocol.startsWith('http')) {
      throw new Error('Cannot look up browser pages');
    }

    return hostname;
  } catch (e) {
    if (e.message.includes('Invalid URL')) {
      throw new Error('Invalid URL detected');
    }
    throw e;
  }
}

The extractDomain function does several important things. It strips the protocol and path, removes the www. prefix (since APIs typically index companies by their root domain), and filters out domains that are clearly not company websites. You don't want your extension trying to look up "google.com" every time someone searches for something.

Pro tip: Consider expanding the skip list based on your users' browsing patterns. If your extension is for sales teams, you might skip CRM domains like salesforce.com and hubspot.com as well, since those are tools, not prospects.

Step 3 — Fetch Company Data from BounceWatch API

Now for the core functionality: calling the BounceWatch company data API to get structured intelligence about the detected domain. We'll handle this in the background service worker to keep API keys secure and enable caching.

Background Service Worker

Create background.js with the API fetching logic. The service worker handles all external requests, which is a security best practice — your API key never touches the page context.

// background.js — API communication layer

const API_BASE = 'https://www.bouncewatch.com/api/v1/extension';

// Listen for messages from the popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'FETCH_COMPANY') {
    fetchCompanyData(message.domain)
      .then(data => sendResponse({ success: true, data }))
      .catch(error => sendResponse({ success: false, error: error.message }));
    return true; // Keep the message channel open for async response
  }

  if (message.type === 'FETCH_SIGNALS') {
    fetchCompanySignals(message.domain)
      .then(data => sendResponse({ success: true, data }))
      .catch(error => sendResponse({ success: false, error: error.message }));
    return true;
  }

  if (message.type === 'TRACK_COMPANY') {
    trackCompany(message.domain)
      .then(data => sendResponse({ success: true, data }))
      .catch(error => sendResponse({ success: false, error: error.message }));
    return true;
  }
});

async function getApiKey() {
  const result = await chrome.storage.local.get('apiKey');
  if (!result.apiKey) {
    throw new Error('NO_API_KEY');
  }
  return result.apiKey;
}

async function fetchCompanyData(domain) {
  const apiKey = await getApiKey();

  // Check cache first
  const cacheKey = `company_${domain}`;
  const cached = await getCachedData(cacheKey);
  if (cached) return cached;

  const response = await fetch(
    `${API_BASE}/company/${encodeURIComponent(domain)}/basic`,
    {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Accept': 'application/json'
      }
    }
  );

  if (!response.ok) {
    if (response.status === 404) {
      throw new Error('Company not found in BounceWatch database');
    }
    if (response.status === 401) {
      throw new Error('Invalid API key. Please check your credentials.');
    }
    if (response.status === 429) {
      throw new Error('Rate limit exceeded. Please wait a moment.');
    }
    throw new Error(`API error: ${response.status}`);
  }

  const data = await response.json();

  // Cache for 1 hour
  await setCachedData(cacheKey, data, 3600000);

  return data;
}

Sample API Response

The BounceWatch API returns structured JSON with company fundamentals and active signals. Here's what a typical response looks like for the /basic endpoint:

{
  "company": {
    "name": "Acme Technologies",
    "domain": "acmetech.io",
    "industry": "Enterprise Software",
    "sub_industry": "Developer Tools",
    "employee_count": 230,
    "employee_range": "201-500",
    "founded_year": 2019,
    "country": "United States",
    "city": "San Francisco",
    "funding_stage": "Series B",
    "total_funding_usd": 42000000,
    "last_funding_date": "2026-01-15",
    "description": "Acme Technologies builds developer productivity tools...",
    "logo_url": "https://www.bouncewatch.com/logos/acmetech-io.png",
    "bouncewatch_url": "https://www.bouncewatch.com/company/acmetech-io"
  },
  "active_signals": [
    {
      "type": "recently_funded",
      "label": "Recently Funded",
      "detected_at": "2026-01-16"
    },
    {
      "type": "hiring_surge",
      "label": "Hiring Surge",
      "detected_at": "2026-02-28"
    }
  ],
  "signal_count": 2
}

This single API call gives you everything needed for a rich intelligence card. The active_signals array is especially valuable — these are the real-time growth indicators that make a company data API for Chrome extensions far more useful than static database lookups. You can learn more about the types of signals available in our recently funded companies and hiring surge signal feeds.

Step 4 — Display the Intelligence Card

With data flowing from the API, let's build the render layer. The intelligence card needs to be compact (the popup window is only about 400px wide) but information-dense.

Popup Styles

Create popup.css with a clean, professional design. The key constraint is the popup width — Chrome limits this to 800px, but anything over 400px feels oversized for a utility tool.

/* popup.css */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  width: 380px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  font-size: 13px;
  color: #1a1a2e;
  background: #ffffff;
}

#app {
  padding: 16px;
}

.state { display: block; }
.hidden { display: none !important; }

/* Company Header */
.company-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}

.company-logo {
  width: 40px;
  height: 40px;
  border-radius: 8px;
  object-fit: contain;
  border: 1px solid #e5e7eb;
}

.company-name {
  font-size: 16px;
  font-weight: 700;
  color: #1a1a2e;
}

.company-industry {
  font-size: 12px;
  color: #6b7280;
}

/* Data Grid */
.data-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-bottom: 16px;
}

.data-item {
  background: #f8f9fa;
  border-radius: 6px;
  padding: 8px 10px;
}

.data-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: #9ca3af;
  margin-bottom: 2px;
}

.data-value {
  font-size: 14px;
  font-weight: 600;
  color: #1a1a2e;
}

/* Signal Badges */
.signal-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 16px;
}

.signal-badge {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: 12px;
  font-size: 11px;
  font-weight: 600;
}

.signal-badge.recently_funded {
  background: #ecfdf5;
  color: #065f46;
}

.signal-badge.hiring_surge {
  background: #eff6ff;
  color: #1e40af;
}

.signal-badge.expanding {
  background: #fefce8;
  color: #854d0e;
}

.signal-badge.new_product {
  background: #f5f3ff;
  color: #5b21b6;
}

/* Actions */
.actions {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}

.btn {
  flex: 1;
  padding: 8px 12px;
  border: none;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: opacity 0.15s;
}

.btn:hover { opacity: 0.85; }

.btn-primary {
  background: #6d3bff;
  color: #ffffff;
}

.btn-secondary {
  background: #f3f4f6;
  color: #374151;
}

Render Function

Back in popup.js, add the render logic that transforms API data into the visual card. This is where the browser extension company data experience comes together.

// popup.js — Rendering the intelligence card

function renderCompanyCard(data) {
  const { company, active_signals } = data;

  const cardEl = document.getElementById('company-card');

  cardEl.innerHTML = `
    <div class="company-header">
      ${company.logo_url
        ? `<img src="${company.logo_url}" alt="${company.name}" class="company-logo">`
        : `<div class="company-logo placeholder">${company.name.charAt(0)}</div>`
      }
      <div>
        <div class="company-name">${escapeHtml(company.name)}</div>
        <div class="company-industry">${escapeHtml(company.industry || 'Unknown Industry')}</div>
      </div>
    </div>

    ${active_signals.length ? `
      <div class="signal-badges">
        ${active_signals.map(signal =>
          `<span class="signal-badge ${signal.type}">${escapeHtml(signal.label)}</span>`
        ).join('')}
      </div>
    ` : ''}

    <div class="data-grid">
      <div class="data-item">
        <div class="data-label">Employees</div>
        <div class="data-value">${formatNumber(company.employee_count)}</div>
      </div>
      <div class="data-item">
        <div class="data-label">Funding Stage</div>
        <div class="data-value">${escapeHtml(company.funding_stage || 'N/A')}</div>
      </div>
      <div class="data-item">
        <div class="data-label">Total Funding</div>
        <div class="data-value">${formatFunding(company.total_funding_usd)}</div>
      </div>
      <div class="data-item">
        <div class="data-label">Founded</div>
        <div class="data-value">${company.founded_year || 'N/A'}</div>
      </div>
      <div class="data-item">
        <div class="data-label">Country</div>
        <div class="data-value">${escapeHtml(company.country || 'N/A')}</div>
      </div>
      <div class="data-item">
        <div class="data-label">Domain</div>
        <div class="data-value">${escapeHtml(company.domain)}</div>
      </div>
    </div>

    <div id="signal-feed"></div>

    <div class="actions">
      <button class="btn btn-primary" id="track-btn">Track Company</button>
      <button class="btn btn-secondary" id="copy-btn">Copy Summary</button>
    </div>
    <div class="actions" style="margin-top: 6px;">
      <a href="${company.bouncewatch_url}" target="_blank" class="btn btn-secondary" style="text-align:center; text-decoration:none;">Open in BounceWatch</a>
    </div>
  `;

  showState('company-card');
  attachCardEventListeners(company);
}

// Utility functions
function escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

function formatNumber(num) {
  if (!num) return 'N/A';
  if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
  return num.toString();
}

function formatFunding(amount) {
  if (!amount) return 'N/A';
  if (amount >= 1000000000) return '$' + (amount / 1000000000).toFixed(1) + 'B';
  if (amount >= 1000000) return '$' + (amount / 1000000).toFixed(0) + 'M';
  if (amount >= 1000) return '$' + (amount / 1000).toFixed(0) + 'K';
  return '$' + amount;
}

function showState(stateId) {
  document.querySelectorAll('.state').forEach(el => el.classList.add('hidden'));
  document.getElementById(stateId).classList.remove('hidden');
}

Notice how we use escapeHtml for all user-facing data. This is a critical security practice when rendering API responses into the DOM. Never use innerHTML with unsanitized data — even data from your own API could contain unexpected characters that break the layout or worse.

Step 5 — Add Signal Feed

The intelligence card shows active signal badges, but your users will want more detail. The signal feed provides a chronological timeline of company events — funding rounds, hiring changes, expansion moves, and more. This is what separates a basic company enrichment tool from a true intelligence platform.

Fetch Signals

Add the signal fetching function to background.js:

// background.js — Signal feed endpoint

async function fetchCompanySignals(domain) {
  const apiKey = await getApiKey();

  const cacheKey = `signals_${domain}`;
  const cached = await getCachedData(cacheKey);
  if (cached) return cached;

  const response = await fetch(
    `${API_BASE}/company/${encodeURIComponent(domain)}/signals`,
    {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Accept': 'application/json'
      }
    }
  );

  if (!response.ok) {
    throw new Error(`Failed to fetch signals: ${response.status}`);
  }

  const data = await response.json();

  // Cache signals for 30 minutes (they change more often)
  await setCachedData(cacheKey, data, 1800000);

  return data;
}

Render the Timeline

In popup.js, add a function to render the signal feed after the main card loads. The timeline design uses a simple vertical layout with color-coded signal types.

// popup.js — Signal feed rendering

async function loadSignalFeed(domain) {
  const feedEl = document.getElementById('signal-feed');

  try {
    const response = await chrome.runtime.sendMessage({
      type: 'FETCH_SIGNALS',
      domain: domain
    });

    if (!response.success || !response.data.signals.length) {
      feedEl.innerHTML = '<p class="no-signals">No recent signals detected.</p>';
      return;
    }

    const signals = response.data.signals.slice(0, 5); // Show last 5

    feedEl.innerHTML = `
      <div class="signal-feed-header">Recent Signals</div>
      <div class="signal-timeline">
        ${signals.map(signal => `
          <div class="signal-item">
            <div class="signal-dot ${signal.type}"></div>
            <div class="signal-content">
              <div class="signal-title">${escapeHtml(signal.title)}</div>
              <div class="signal-date">${formatDate(signal.detected_at)}</div>
            </div>
          </div>
        `).join('')}
      </div>
    `;
  } catch (error) {
    feedEl.innerHTML = '<p class="no-signals">Could not load signals.</p>';
  }
}

function formatDate(dateStr) {
  const date = new Date(dateStr);
  const now = new Date();
  const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));

  if (diffDays === 0) return 'Today';
  if (diffDays === 1) return 'Yesterday';
  if (diffDays < 7) return `${diffDays} days ago`;
  if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;

  return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}

The signal feed transforms your extension from a static data card into a dynamic intelligence tool. Sales teams using the BounceWatch Signal Tracker already know the value of real-time signals — the extension just puts that data where they're already working.

Step 6 — Add Quick Actions

A great sales chrome extension doesn't just show data — it helps you act on it. We'll add three quick actions: tracking a company, opening it in BounceWatch, and copying a summary to the clipboard.

Track This Company

The track button adds the company to the user's BounceWatch watchlist so they get notified about future signals. Add this to background.js:

// background.js — Track company endpoint

async function trackCompany(domain) {
  const apiKey = await getApiKey();

  const response = await fetch(
    `${API_BASE}/company/${encodeURIComponent(domain)}/track`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      }
    }
  );

  if (!response.ok) {
    if (response.status === 409) {
      return { already_tracked: true };
    }
    throw new Error(`Failed to track company: ${response.status}`);
  }

  return await response.json();
}

Event Listeners for Actions

In popup.js, wire up the action buttons. The copy function generates a clean text summary that sales reps can paste into CRM notes, Slack, or emails.

// popup.js — Quick action handlers

function attachCardEventListeners(company) {
  // Track button
  document.getElementById('track-btn').addEventListener('click', async () => {
    const btn = document.getElementById('track-btn');
    btn.textContent = 'Tracking...';
    btn.disabled = true;

    try {
      const response = await chrome.runtime.sendMessage({
        type: 'TRACK_COMPANY',
        domain: company.domain
      });

      if (response.success) {
        btn.textContent = response.data.already_tracked
          ? 'Already Tracked'
          : 'Tracked!';
        btn.classList.add('btn-success');
      } else {
        btn.textContent = 'Failed';
        setTimeout(() => {
          btn.textContent = 'Track Company';
          btn.disabled = false;
        }, 2000);
      }
    } catch (error) {
      btn.textContent = 'Error';
      btn.disabled = false;
    }
  });

  // Copy summary button
  document.getElementById('copy-btn').addEventListener('click', async () => {
    const summary = generateSummary(company);

    try {
      await navigator.clipboard.writeText(summary);
      const btn = document.getElementById('copy-btn');
      btn.textContent = 'Copied!';
      setTimeout(() => btn.textContent = 'Copy Summary', 1500);
    } catch (err) {
      console.error('Failed to copy:', err);
    }
  });
}

function generateSummary(company) {
  const parts = [
    company.name,
    company.industry ? `Industry: ${company.industry}` : null,
    company.employee_count ? `Employees: ${company.employee_count}` : null,
    company.funding_stage ? `Stage: ${company.funding_stage}` : null,
    company.total_funding_usd ? `Funding: ${formatFunding(company.total_funding_usd)}` : null,
    company.country ? `Location: ${company.city ? company.city + ', ' : ''}${company.country}` : null,
    company.founded_year ? `Founded: ${company.founded_year}` : null,
    `\nMore: ${company.bouncewatch_url}`
  ];

  return parts.filter(Boolean).join('\n');
}

The generated summary is designed to be immediately useful. When a sales rep copies it, they get a clean text block ready to paste into Salesforce notes, a Slack channel, or a prospecting spreadsheet. No reformatting needed.

Polishing the Extension

With the core functionality built, let's add the polish that separates a prototype from a production-ready extension. These details matter for user trust and daily usability.

Caching Layer

API calls should be cached to reduce latency and avoid hitting rate limits. We used cache helpers in the code above — here's the implementation using chrome.storage.local:

// background.js — Caching utilities

async function getCachedData(key) {
  const result = await chrome.storage.local.get(key);
  const cached = result[key];

  if (!cached) return null;

  // Check if cache has expired
  if (Date.now() > cached.expiry) {
    await chrome.storage.local.remove(key);
    return null;
  }

  return cached.data;
}

async function setCachedData(key, data, ttlMs) {
  await chrome.storage.local.set({
    [key]: {
      data: data,
      expiry: Date.now() + ttlMs
    }
  });
}

// Clear old cache entries periodically
chrome.alarms.create('cache-cleanup', { periodInMinutes: 60 });

chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'cache-cleanup') {
    cleanExpiredCache();
  }
});

async function cleanExpiredCache() {
  const all = await chrome.storage.local.get(null);
  const now = Date.now();
  const keysToRemove = [];

  for (const [key, value] of Object.entries(all)) {
    if (value?.expiry && now > value.expiry) {
      keysToRemove.push(key);
    }
  }

  if (keysToRemove.length) {
    await chrome.storage.local.remove(keysToRemove);
  }
}

Error Handling

Robust error handling is critical for browser extensions because users can't see console errors. Every failure state needs a visible, helpful message.

// popup.js — Error handling

function showError(message, retryFn) {
  const errorEl = document.getElementById('error');

  errorEl.innerHTML = `
    <div class="error-icon">!</div>
    <p class="error-message">${escapeHtml(message)}</p>
    ${retryFn ? '<button class="btn btn-secondary" id="retry-btn">Try Again</button>' : ''}
  `;

  showState('error');

  if (retryFn) {
    document.getElementById('retry-btn').addEventListener('click', retryFn);
  }
}

Loading States

Never leave the user staring at a blank popup. Show a spinner immediately, and consider adding a skeleton screen for the card layout. The CSS spinner is lightweight and doesn't require any external dependencies:

/* popup.css — Loading spinner */
.spinner {
  width: 32px;
  height: 32px;
  border: 3px solid #e5e7eb;
  border-top-color: #6d3bff;
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
  margin: 40px auto 16px;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

Rate Limiting

Even with caching, you should implement client-side rate limiting as a safety net. This prevents accidental API abuse if a user rapidly clicks through tabs:

// background.js — Simple rate limiter

const requestTimestamps = [];
const MAX_REQUESTS_PER_MINUTE = 30;

function checkRateLimit() {
  const now = Date.now();
  const oneMinuteAgo = now - 60000;

  // Remove old timestamps
  while (requestTimestamps.length && requestTimestamps[0] < oneMinuteAgo) {
    requestTimestamps.shift();
  }

  if (requestTimestamps.length >= MAX_REQUESTS_PER_MINUTE) {
    throw new Error('Rate limit: too many requests. Please wait a moment.');
  }

  requestTimestamps.push(now);
}

Main Initialization

Tie everything together with the main initialization flow in popup.js:

// popup.js — Main initialization

document.addEventListener('DOMContentLoaded', async () => {
  try {
    // Check for API key first
    const { apiKey } = await chrome.storage.local.get('apiKey');
    if (!apiKey) {
      showState('no-api-key');
      setupApiKeyForm();
      return;
    }

    // Get current domain
    const domain = await getCurrentDomain();

    // Fetch company data
    const response = await chrome.runtime.sendMessage({
      type: 'FETCH_COMPANY',
      domain: domain
    });

    if (response.success) {
      renderCompanyCard(response.data);
      // Load signal feed in the background
      loadSignalFeed(domain);
    } else {
      showError(response.error, () => location.reload());
    }
  } catch (error) {
    showError(error.message);
  }
});

function setupApiKeyForm() {
  document.getElementById('save-key-btn').addEventListener('click', async () => {
    const key = document.getElementById('api-key-input').value.trim();
    if (!key) return;

    await chrome.storage.local.set({ apiKey: key });
    location.reload(); // Restart the flow with the new key
  });
}

Publishing to Chrome Web Store

Once your extension is tested and polished, publishing it to the Chrome Web Store makes it available to your team or the public. Here's the process at a high level:

  1. Create a developer account at the Chrome Web Store Developer Dashboard. There's a one-time $5 registration fee.
  2. Prepare your assets:
    • Extension icons (16x16, 48x48, 128x128 pixels)
    • At least one screenshot (1280x800 or 640x400)
    • A promotional tile image (440x280)
    • A detailed description for the store listing
  3. Package your extension as a ZIP file containing all your source files (manifest.json, JS, CSS, HTML, icons).
  4. Upload and submit through the developer dashboard. Google reviews extensions before publishing, which typically takes 1-3 business days.
  5. For internal distribution, you can use Chrome's enterprise policies or share the unpacked extension directly with your team during development.

Security note: Chrome Web Store reviews check for data handling practices. Since our extension only sends data to the BounceWatch API (which the user explicitly authenticated with), and stores the API key locally with chrome.storage.local, it aligns with Chrome's security requirements. Never bundle API keys in the extension source — always let users provide their own credentials. For more on best practices, check out the web.dev security guides.

Where to Go from Here

You've built a fully functional Chrome extension that turns any company website into an intelligence briefing. Here are some ideas to take it further:

  • LinkedIn integration — detect company pages on LinkedIn and show your intelligence card alongside the native profile
  • Batch lookup — let users select multiple domains from a page (e.g., a list of startups on a blog) and fetch data for all of them
  • CRM push — add a "Send to Salesforce" or "Send to HubSpot" button that creates a lead record with the enriched data. Read more about building enrichment pipelines for CRM integration patterns.
  • Notification alerts — use Chrome's notification API to alert users when a tracked company triggers a new signal
  • Comparison view — if the user has recently looked up multiple companies, show a side-by-side comparison

The BounceWatch API powers each of these features with the same endpoints you've already integrated. Whether you're building a sales chrome extension for your team or a public tool for the developer community, the pattern stays the same: detect context, fetch intelligence, display actionably.

For a deeper comparison of company data providers for your extension, see our guide on the best company enrichment APIs in 2026 and our Clearbit alternative analysis. If you're exploring how signal data drives sales outcomes, the Signal Tracker for Sales Teams page breaks down the full workflow.

You can find more developer resources and community discussions on dev.to, Stack Overflow, and CSS-Tricks for front-end styling inspiration.

Ready to build? Get your BounceWatch API key and start adding company intelligence to your browser extension today. From domain detection to signal feeds, you've got the complete blueprint.

Chrome Extension Browser Extension Company Data API Developer Tutorial Sales Tools JavaScript
Share
Bounce Watch

Bounce Watch Team

Published on March 08, 2026

All Articles

Access startup data via our powerful API

Enrich your CRM, build integrations, and automate company research with the Bounce Watch API.

One analytics cookie (Google Analytics). Cookie Policy