Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Most chrome.tabs.captureVisibleTab() “permission denied” errors have one of four causes: the extension lacks activeTab or <all_urls>, the call is not tied to a recent user action, the target is a restricted or file URL, or the call is being made from a content script. Add the least-broad permission your feature needs, invoke the API from an extension page or service worker immediately after user activation, and handle pages Chrome does not expose.

The permission Chrome actually requires

Chrome’s tabs API requires captureVisibleTab() to run with either the activeTab permission or the broad <all_urls> host permission. The separate tabs permission is not the permission that unlocks this method; it exposes sensitive fields such as a tab’s URL, title and favicon.

Use activeTab for a user-triggered screenshot

For an extension whose screenshot starts when the user clicks its action button, chooses a context-menu item, presses a registered shortcut or selects an omnibox suggestion, Manifest V3 normally needs:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
{
  "manifest_version": 3,
  "name": "Visible tab capture",
  "version": "1.0.0",
  "permissions": ["activeTab"],
  "background": { "service_worker": "service-worker.js" },
  "action": { "default_title": "Capture visible tab" }
}

activeTab grants temporary access to the tab in which the user acted. The grant ends when the tab is closed or the user navigates to a different origin. It is therefore a poor fit for an unrelated alarm, delayed job or background event that may run after the user’s access has expired.

Use <all_urls> only when the product needs broad host access

If your feature must capture arbitrary sites without a fresh user invocation, declare the broad host permission instead:

{
  "permissions": ["<all_urls>"],
  "background": { "service_worker": "service-worker.js" }
}

This gives the extension wider reach and a more prominent installation warning. Chrome’s permission-minimization guidance favors optional or narrower permissions when the feature permits it. Do not add tabs simply to fix a capture error.

Call the API from the right extension context

chrome.tabs.captureVisibleTab() is available to extension pages and the Manifest V3 service worker, not to content scripts. A content script can request a capture, but an extension context must perform the actual call.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Service-worker implementation

chrome.action.onClicked.addListener(async (tab) => {
  if (!tab.id) return;

  try {
    const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
      format: "png"
    });
    console.log("Captured image length:", dataUrl.length);
    // Store, download, or send dataUrl to an extension page.
  } catch (error) {
    console.error("captureVisibleTab failed:", error);
  }
});

The action click is the user invocation that activates activeTab. If a popup starts the operation, send a message to the service worker and capture there:

// popup.js
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab?.id) {
  chrome.runtime.sendMessage({ type: "capture", windowId: tab.windowId });
}

// service-worker.js
chrome.runtime.onMessage.addListener((message, sender) => {
  if (message.type !== "capture") return;
  chrome.tabs.captureVisibleTab(message.windowId, { format: "png" })
    .then((dataUrl) => console.log(dataUrl))
    .catch((error) => console.error(error));
});

When a content script is involved, use the same message pattern. Never attempt to call the tabs API directly in that script.

A reliable troubleshooting sequence

1. Inspect the loaded manifest

Open chrome://extensions, enable Developer mode, and click Errors or reload the unpacked extension. Confirm that the manifest actually loaded with "activeTab" or "<all_urls>" under permissions. Editing a source file without reloading the extension leaves Chrome running the old manifest.

2. Prove that a user action precedes capture

Put the capture directly in an action click, context-menu callback, shortcut handler or omnibox callback while diagnosing. Do not start with a timer, alarm, delayed promise chain or arbitrary message from a web page. With activeTab, the temporary grant belongs to the tab and origin where the action occurred. A navigation to another origin or closing the tab removes it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

3. Check the target URL

Ordinary HTTP and HTTPS pages are the normal case. Browser-internal and extension pages are different:

  • chrome:// pages: activeTab does not grant access to restricted pages such as Chrome’s settings and other browser-internal screens. Do not promise users that every Chrome page can be captured.
  • Other extensions’ pages and data: URLs: Chrome documents these as capturable with activeTab, but they remain subject to the API’s context and invocation rules.
  • file: URLs: the user must enable Allow access to file URLs on the extension’s details page. This is a separate user-controlled setting; adding activeTab alone does not switch it on.

Log tab.url where your extension is allowed to read it, and show a clear message when the user is on a restricted page instead of retrying indefinitely.

4. Confirm the calling context

If the stack trace originates in a content script, move the call to the service worker, popup, options page or another extension page and communicate through chrome.runtime.sendMessage. The tabs API overview distinguishes those extension contexts from content scripts.

5. Stay below Chrome’s capture limit

Chrome documents a maximum of two captureVisibleTab() calls per second (the MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND limit). Capture is expensive. A loop that requests three or more images in one second can fail or become unreliable even when permissions are correct.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
async function captureWithSpacing(windowId, count) {
  const shots = [];
  for (let i = 0; i < count; i++) {
    shots.push(await chrome.tabs.captureVisibleTab(windowId, { format: "png" }));
    if (i + 1 < count) {
      await new Promise(resolve => setTimeout(resolve, 550));
    }
  }
  return shots;
}

For long sequences, queue requests and handle rejection rather than firing parallel calls.

Permission errors that look similar

Symptom Likely cause Fix
“Cannot access contents of the page” or permission denied immediately after installation No activeTab or <all_urls> in the loaded manifest Add the appropriate permission, reload the extension, then retry from a user action
Works from the toolbar but fails from an alarm or delayed task The temporary activeTab grant has expired or was never created Capture during the user gesture, or redesign around a broader host permission if that is genuinely required
Fails only on Chrome settings, extensions or other browser-internal screens Restricted page Explain that the page cannot be captured through this flow; offer an ordinary web page as a test
Fails only for local documents File access is disabled for the extension Open the extension details and enable Allow access to file URLs
Content-script stack trace Wrong API context Message an extension page or service worker and call the API there
Intermittent failures during a gallery or loop More than two calls per second Serialize requests and space them at least roughly half a second apart

Capture options and implementation details

The second argument can specify an image format such as png, jpeg or webp where supported by your Chrome version and extension code. The method captures the currently visible area of the tab, not a full, scrolling page. If you need a page image rather than the viewport, use a different workflow that scrolls and stitches content, subject to the same permission and context constraints.

Handle the returned data URL as binary image data. For a download from an extension page, create an anchor with the data URL and click it, or pass the data to a service worker that uses the downloads API. Avoid logging very large data URLs in production. Catch rejected promises and provide a user-facing reason; a silent retry cannot overcome a restricted URL or missing grant.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is a dependable website screenshot rather than an in-browser extension feature, ScreenshotNeo provides a one-call API. It accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use the API documentation at https://screenshotneo.com/docs/ for authentication and options. A minimal request is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo supports PNG, JPEG, WebP and PDF output; full-page capture with lazy images, CSS-selector element capture, dark mode, device presets, custom viewport and retina scale, PDF paper settings, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, a usage API and an OpenAPI specification. Its parameter names are compatible with those used by other screenshot APIs, which can simplify migration.

Every plan includes every feature. The Free plan provides 1,000 screenshots per month without a card; paid plans are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000 and Business $249 for 1,000,000. Yearly billing gives two months free. Create a free ScreenshotNeo account to start with the 1,000 monthly screenshots and no card.

FAQ

Do I need the tabs permission or activeTab?

For captureVisibleTab(), use activeTab for a user-triggered flow or <all_urls> for genuinely broad access. The tabs permission serves a different purpose: sensitive tab properties.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can an extension capture a chrome:// page?

Not through the ordinary activeTab flow. Chrome classifies browser-internal pages as restricted, so design your UI to explain that limitation.

Why does the same code work on one site but not another?

The temporary grant is tied to the tab and origin where the user invoked the extension. Navigation to a different origin ends that grant, and restricted or file URLs have additional controls.

Frequently Asked Questions

Does adding host permissions make content scripts able to call captureVisibleTab()?

No. Host permissions affect access to pages, but the tabs API call still has to run from an extension page or service worker.

What is the safest way to test a permission fix?

Reload the extension at chrome://extensions, open a normal HTTPS page, click the extension action, and capture once before testing delayed jobs or restricted URLs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Bottom Line

Declare activeTab for a screenshot started by the user, call captureVisibleTab() from an extension context, account for restricted and file URLs, and keep requests under two per second. Use ScreenshotNeo when you need website captures without maintaining browser-extension permissions.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.