TL;DRBuild a Chrome extension that humanizes selected text via right-click context menu. The extension captures selection, calls the AI Humanizer API from the background script, and replaces the text in the active tab. Manifest V3 compatible, no proxy server required.

How to Build a Chrome Extension for AI Text Humanization

Chrome Extensions provide a powerful way to bring AI humanization directly into users’ workflows. By adding humanization capabilities to any webpage, you create friction-free access to the Humanizer API. This guide walks you through building a production-ready extension from manifest configuration through Web Store deployment.

Understanding Chrome Extension Architecture

Chrome Extensions follow a specific architecture with clear boundaries between content scripts, popup UI, background service workers, and manifest configuration. Understanding these layers helps you build secure and performant extensions.

The manifest.json file is your extension’s configuration blueprint. It declares permissions, defines script locations, and specifies how your extension interacts with pages. Manifest v3 is the current standard and offers improved security and performance over earlier versions.

Content scripts run in the context of web pages and can access page DOM. Background service workers handle long-running tasks and API communication. Popup scripts provide user-facing UI. This separation of concerns keeps your extension secure and efficient.

Setting Up Your Manifest v3 Configuration

Start by creating your manifest.json file. This configuration declares all permissions and entry points for your extension.

{
  "manifest_version": 3,
  "name": "AI Text Humanizer",
  "version": "1.0.0",
  "description": "Transform AI-generated text into natural human writing with one click",
  "permissions": [
    "storage",
    "scripting"
  ],
  "host_permissions": [
    "https://*/*",
    "http://*/*"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_title": "AI Text Humanizer"
  },
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": [""],
      "js": ["content.js"],
      "run_at": "document_end"
    }
  ],
  "icons": {
    "16": "images/icon-16.png",
    "48": "images/icon-48.png",
    "128": "images/icon-128.png"
  }
}

The manifest declares you need storage permissions for saving settings and the API key, scripting permissions for modifying pages, and broad host permissions to run on any website. The action field defines your popup, background specifies the service worker, and content_scripts define where injection happens.

Building the Content Script

Content scripts inject humanization capabilities directly into web pages. They detect text selection and communicate with the background worker to fetch humanized versions.

// content.js
interface HumanizeMessage {
  action: 'humanize';
  text: string;
  tone: 'professional' | 'casual' | 'academic';
}

interface HumanizeResponse {
  success: boolean;
  humanized_text?: string;
  error?: string;
}

// Listen for context menu or selection events
document.addEventListener('mouseup', () => {
  const selectedText = window.getSelection()?.toString().trim();

  if (selectedText && selectedText.length > 10) {
    // Show indicator that text can be humanized
    chrome.runtime.sendMessage({
      action: 'textSelected',
      length: selectedText.length,
    });
  }
});

// Listen for messages from the popup or background worker
chrome.runtime.onMessage.addListener(
  (message: HumanizeMessage, sender, sendResponse) => {
    if (message.action === 'humanize') {
      // Send to background worker for API call
      chrome.runtime.sendMessage(message, (response: HumanizeResponse) => {
        if (response.success) {
          replaceSelectedText(response.humanized_text || '');
          sendResponse({ success: true });
        } else {
          sendResponse({ success: false, error: response.error });
        }
      });
      return true; // Keep channel open for async response
    }
  }
);

function replaceSelectedText(newText: string): void {
  const selection = window.getSelection();
  if (selection && selection.rangeCount > 0) {
    const range = selection.getRangeAt(0);
    const textNode = document.createTextNode(newText);
    range.deleteContents();
    range.insertNode(textNode);
    selection.removeAllRanges();
  }
}

This content script listens for text selection, detects when meaningful text is highlighted, and provides messaging channels to the background worker. When humanization completes, it replaces selected text directly in the page.

Creating the Popup UI

The popup provides quick access to humanization with tone selection and result display. Create a simple HTML file with embedded styling.

// popup.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>AI Text Humanizer</title>
  <style>
    body {
      width: 400px;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      margin: 0;
      padding: 16px;
      background: #f9fafb;
    }
    .container {
      display: flex;
      flex-direction: column;
      gap: 12px;
    }
    textarea {
      width: 100%;
      height: 100px;
      padding: 8px;
      border: 1px solid #d1d5db;
      border-radius: 6px;
      font-family: inherit;
      resize: vertical;
      box-sizing: border-box;
    }
    .controls {
      display: flex;
      gap: 8px;
      align-items: center;
    }
    select {
      padding: 6px 8px;
      border: 1px solid #d1d5db;
      border-radius: 4px;
    }
    button {
      flex: 1;
      padding: 8px 12px;
      background: #2563eb;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-weight: 500;
    }
    button:hover {
      background: #1d4ed8;
    }
    button:disabled {
      background: #9ca3af;
      cursor: not-allowed;
    }
    .result {
      padding: 8px;
      background: #f0fdf4;
      border: 1px solid #86efac;
      border-radius: 4px;
      display: none;
    }
    .result.show {
      display: block;
    }
    .error {
      padding: 8px;
      background: #fef2f2;
      border: 1px solid #fca5a5;
      color: #dc2626;
      border-radius: 4px;
      display: none;
    }
    .error.show {
      display: block;
    }
    .status {
      font-size: 12px;
      color: #6b7280;
    }
  </style>
</head>
<body>
  <div class="container">
    <h2 style="margin: 0 0 8px 0; font-size: 16px;">AI Text Humanizer</h2>

    <textarea id="input" placeholder="Paste or type AI-generated text here..."></textarea>

    <div class="controls">
      <select id="tone">
        <option value="professional">Professional</option>
        <option value="casual">Casual</option>
        <option value="academic">Academic</option>
      </select>
      <button id="humanize">Humanize</button>
    </div>

    <div id="result" class="result"></div>
    <div id="error" class="error"></div>
    <div id="status" class="status"></div>
  </div>

  <script src="popup.js"></script>
</body>
</html>

The popup provides a straightforward interface for pasting text and selecting tone. When results come back from the background worker, they display in a highlighted section. Error messages appear in a distinct color to guide users.

Implementing the Background Service Worker

The background service worker handles API communication, API key management, and message routing between content scripts and the popup.

// background.js
interface StoredConfig {
  apiKey: string;
  apiUrl: string;
}

interface HumanizeRequest {
  text: string;
  tone: 'professional' | 'casual' | 'academic';
}

interface HumanizeResponse {
  success: boolean;
  humanized_text?: string;
  error?: string;
}

// Listen for messages from content scripts and popup
chrome.runtime.onMessage.addListener(
  (message: any, sender, sendResponse) => {
    if (message.action === 'humanize') {
      humanizeText(message.text, message.tone)
        .then((result) => sendResponse(result))
        .catch((error) => {
          console.error('Humanization failed:', error);
          sendResponse({
            success: false,
            error: error.message || 'API request failed',
          });
        });
      return true; // Keep channel open for async response
    }

    if (message.action === 'getConfig') {
      getConfig().then((config) => sendResponse(config));
      return true;
    }

    if (message.action === 'setConfig') {
      setConfig(message.apiKey, message.apiUrl)
        .then(() => sendResponse({ success: true }))
        .catch((error) => sendResponse({ success: false, error: error.message }));
      return true;
    }
  }
);

async function humanizeText(
  text: string,
  tone: string
): Promise {
  const config = await getConfig();

  if (!config.apiKey) {
    throw new Error('API key not configured. Visit the extension settings.');
  }

  if (!text || text.trim().length === 0) {
    throw new Error('Text cannot be empty');
  }

  try {
    const response = await fetch(`${config.apiUrl}/humanize`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${config.apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ text, tone, preserve_formatting: true }),
      signal: AbortSignal.timeout(30000),
    });

    if (response.status === 429) {
      throw new Error('Rate limit exceeded. Please try again later.');
    }

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.error || 'Humanization failed');
    }

    const data = await response.json();
    return {
      success: true,
      humanized_text: data.humanized_text,
    };
  } catch (error) {
    if (error instanceof TypeError) {
      throw new Error('Network error. Check your connection and API URL.');
    }
    throw error;
  }
}

async function getConfig(): Promise {
  return new Promise((resolve) => {
    chrome.storage.sync.get(
      {
        apiKey: '',
        apiUrl: 'https://api.aihumanizerapi.com/v1',
      },
      resolve
    );
  });
}

async function setConfig(apiKey: string, apiUrl: string): Promise {
  return new Promise((resolve, reject) => {
    chrome.storage.sync.set({ apiKey, apiUrl }, () => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
      } else {
        resolve();
      }
    });
  });
}

The background worker is the security boundary of your extension. It holds API credentials in Chrome’s encrypted storage and never exposes them to content scripts. All API communication flows through the worker, preventing credentials from leaking to untrusted websites.

Building the Popup Script

The popup script handles user interaction and communicates with the background worker.

// popup.js
const inputEl = document.getElementById('input') as HTMLTextAreaElement;
const toneEl = document.getElementById('tone') as HTMLSelectElement;
const humanizeBtn = document.getElementById('humanize') as HTMLButtonElement;
const resultEl = document.getElementById('result') as HTMLDivElement;
const errorEl = document.getElementById('error') as HTMLDivElement;
const statusEl = document.getElementById('status') as HTMLDivElement;

humanizeBtn.addEventListener('click', async () => {
  const text = inputEl.value.trim();
  if (!text) {
    showError('Please enter some text to humanize');
    return;
  }

  humanizeBtn.disabled = true;
  statusEl.textContent = 'Humanizing...';
  resultEl.classList.remove('show');
  errorEl.classList.remove('show');

  try {
    const response = await chrome.runtime.sendMessage({
      action: 'humanize',
      text,
      tone: toneEl.value,
    });

    if (response.success) {
      resultEl.textContent = response.humanized_text;
      resultEl.classList.add('show');
      statusEl.textContent = 'Done! You can copy or paste the result.';
    } else {
      showError(response.error || 'Humanization failed');
    }
  } catch (error) {
    showError((error as Error).message);
  } finally {
    humanizeBtn.disabled = false;
  }
});

function showError(message: string): void {
  errorEl.textContent = message;
  errorEl.classList.add('show');
  statusEl.textContent = '';
}

Secure API Key Storage

Never hardcode API keys in your extension code. Chrome’s storage.sync API provides encrypted storage that syncs across a user’s devices.

Create a simple settings page where users can configure their API key.

// settings.html
<!DOCTYPE html>
<html>
<head>
  <title>AI Text Humanizer Settings</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      max-width: 600px;
      margin: 40px auto;
      padding: 20px;
    }
    input {
      width: 100%;
      padding: 8px;
      margin: 8px 0;
      border: 1px solid #ccc;
      border-radius: 4px;
      box-sizing: border-box;
    }
    button {
      padding: 10px 20px;
      background: #2563eb;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    button:hover {
      background: #1d4ed8;
    }
    .status {
      margin-top: 16px;
      padding: 12px;
      border-radius: 4px;
    }
    .success {
      background: #dcfce7;
      color: #166534;
    }
    .error {
      background: #fee2e2;
      color: #991b1b;
    }
  </style>
</head>
<body>
  <h3>AI text humanizer settings</h3>

  <div>
    <label for="apiKey">API Key:</label>
    <input
      type="password"
      id="apiKey"
      placeholder="Your Humanizer API key"
    >
  </div>

  <div>
    <label for="apiUrl">API URL:</label>
    <input
      type="url"
      id="apiUrl"
      placeholder="https://api.aihumanizerapi.com/v1"
      value="https://api.aihumanizerapi.com/v1"
    >
  </div>

  <button id="save">Save Settings</button>
  <div id="status"></div>

  <script src="settings.js"></script>
</body>
</html>

Add this page to your manifest under action.default_popup or as a dedicated settings page. Users visit this page to securely store their API credentials.

Handling Permissions Properly

Manifest v3 requires explicit user consent for sensitive permissions. Your extension should handle permission requests gracefully.

// Check if extension has required permissions before using features
async function requestRequiredPermissions(): Promise {
  try {
    const hasPermissions = await chrome.permissions.contains({
      permissions: ['storage'],
    });

    if (!hasPermissions) {
      const granted = await chrome.permissions.request({
        permissions: ['storage'],
      });
      return granted;
    }

    return true;
  } catch (error) {
    console.error('Permission request failed:', error);
    return false;
  }
}

// Call this when extension first loads
requestRequiredPermissions().catch(console.error);

Publishing to the Chrome Web Store

Before submitting, prepare your extension package and store listing.

First, create a .zip file containing all your extension files: manifest.json, popup.html, popup.js, content.js, background.js, settings.html, settings.js, and images folder. Do not include node_modules or build artifacts.

Then follow these steps. Create a Chrome Web Store developer account at chromewebstore.google.com. Pay the one-time $5 registration fee. Go to your developer dashboard and click “New item”. Upload your .zip file and fill in the required fields: detailed description, screenshots showing your extension in action, category (Productivity), language, and icon (128×128 PNG).

Write a compelling description focusing on user benefits. For the Humanizer API extension, emphasize how users transform AI-generated content without leaving their current page. Include clear screenshots showing selected text being humanized and the results appearing in-page.

Submit for review. Google typically reviews extensions within 24 hours. They verify your extension follows policies, doesn’t contain malware, and doesn’t violate user privacy. Common rejection reasons include accessing data without consent, misleading descriptions, or interference with other extensions.

Security Best Practices

Handle user data carefully to maintain trust.

Never log sensitive data like API keys or user text. Store credentials only in chrome.storage.sync with encryption. Never send user text to third-party analytics. Use HTTPS for all API communication. Validate all inputs from content scripts before using them. Never grant overly broad host_permissions if you can be more specific. Request permissions only when needed, not at install time.

Review the API documentation for rate limits and security requirements. Your extension should respect rate limits to avoid service disruption for your users.

Testing Your Extension

Before deploying, test thoroughly on multiple sites.

Load your extension locally by going to chrome://extensions, enabling “Developer mode”, and clicking “Load unpacked” to select your extension folder. Test on websites with different content types: blogs, social media, email, documents. Verify humanization works correctly with different text lengths and tone options. Test error scenarios like network failures and missing API keys. Check that selected text replacement works on all sites. Verify API key doesn’t leak in console logs or network requests.

Monitoring and Updates

After launch, monitor extension usage and user feedback.

Review user ratings and comments regularly. Monitor error logs from users reporting issues. Push updates to fix bugs and add features. Keep dependencies updated for security patches. Monitor API usage to identify performance bottlenecks. Collect metrics on most-used tones and text lengths to guide future improvements.

Next Steps

You now have everything needed to build a production-ready Chrome Extension for the Humanizer API. This extension brings humanization capabilities to thousands of websites users visit daily, creating frictionless access to AI text transformation. For more details on API capabilities, visit our API documentation.

Ready to launch your extension? Get a free API key and start with 10,000 words per month, no credit card required.