How to send a screenshot API request from a browser extension
Capture a Chrome tab, turn its data URL into an upload, and send it safely from a Manifest V3 extension to a screenshot API.
To send a screenshot from a Chrome extension to an API, call chrome.tabs.captureVisibleTab() after a user action, convert its data URL to a Blob, and upload it with fetch() from your extension service worker or extension page. Declare activeTab and host permission for the API. The capture contains only the active tab’s visible area; the API documentation defines the upload field, authentication, formats, limits, and response.
1. Choose the capture and upload contract
Before writing the upload code, check the receiving API’s documentation for its HTTP method, endpoint, expected body (multipart, raw image bytes, or JSON/base64), field names, authentication, accepted image formats, maximum payload size, and response format. Chrome creates an image; it does not define the server’s upload contract.
| Decision | What to account for |
|---|---|
| Capture scope | captureVisibleTab captures the visible viewport in the active tab, not a full-page image. |
| Image format | Use a format the receiving API accepts. Chrome’s capture API supports PNG and JPEG output. |
| Upload encoding | Use the exact body contract documented by the endpoint. Multipart, raw bytes, and JSON/base64 are not interchangeable. |
| Authentication | Use the scheme and credential handling required by the API. Avoid embedding a long-lived secret in an extension distributed to users. |
Chrome documents a maximum of two captureVisibleTab calls per second. Design around that limit if users can request repeated captures. Chrome tabs API documentation.
2. Declare the minimum permissions
For a user-invoked capture, activeTab is generally the narrow permission to start with. Add a host permission for the API origin so extension-owned code can make the cross-origin request. Replace the example host with the exact API hostname.
{
"manifest_version": 3,
"name": "Screenshot uploader",
"version": "1.0.0",
"permissions": ["activeTab"],
"host_permissions": ["https://api.example.com/*"],
"background": {
"service_worker": "service-worker.js"
},
"action": {
"default_title": "Capture and upload tab"
}
}
The documented permission alternatives for captureVisibleTab are activeTab and <all_urls>. Prefer the narrower permission when it fits the user-invoked workflow. Chrome also supports optional host permissions, which can be requested at runtime where that consent model suits the extension. Tabs API permissions · Declare permissions.
3. Capture and upload from the service worker
This Manifest V3 example handles a toolbar click, captures the active tab, converts the data URL to a blob, and sends multipart form data. The field name and authorization shown are placeholders: replace them to match your API. The example assumes the server returns JSON.
const API_URL = "https://api.example.com/v1/screenshots";
chrome.action.onClicked.addListener(async (tab) => {
if (!tab.windowId) return;
try {
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
format: "png"
});
const imageBlob = await (await fetch(dataUrl)).blob();
const form = new FormData();
form.append("screenshot", imageBlob, "screenshot.png");
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${await getApiToken()}`
},
body: form
});
if (!response.ok) {
throw new Error(`Upload failed: HTTP ${response.status}`);
}
const result = await response.json();
console.log("Screenshot uploaded", result);
} catch (error) {
console.error("Capture or upload failed", error);
}
});
async function getApiToken() {
// Obtain this through your product's approved sign-in or token flow.
// Do not put a shared, unrestricted secret in a public extension bundle.
return "USER_SCOPED_TOKEN";
}
Do not manually set the Content-Type header when sending FormData. The browser must add the multipart boundary. If the endpoint expects raw binary or JSON/base64, follow its documented format instead of this multipart example. The service worker is event-driven and may become dormant, so finish the work within the event handler and report the result to the UI rather than relying on in-memory state persisting. Extension service workers.
4. Keep network access and credentials constrained
Make the request from the service worker or an extension page. Content scripts run in the context of the page and remain subject to its same-origin restrictions; declaring a host permission does not give a content script unrestricted cross-origin fetch access. Chrome’s networking guide recommends extension-owned contexts for cross-origin requests. Cross-origin network requests.
- Use a fixed, trusted API URL in extension code. Do not accept an arbitrary fetch URL from a webpage message.
- Validate the sender of messages received by the service worker and expose only the specific capture/upload operation needed.
- Use HTTPS and avoid logging image bytes, tokens, or sensitive response data.
- Make capture a clear user action and explain when the screenshot is uploaded. A visible tab can contain personal or confidential information.
- Use user-scoped or short-lived credentials when the API supports them. An extension package is inspectable, so a bundled shared secret should not be treated as private.
Chrome specifically warns that an extension message handler which lets a page choose an arbitrary fetch destination can create an access-control vulnerability. Chrome networking guidance.
5. Handle API responses and upload formats
Check response.ok before parsing a success body. Some APIs return JSON, others return an image, an identifier, or an empty response. Match the parser to the documented response:
if (!response.ok) {
const detail = await response.text();
throw new Error(`Upload failed (${response.status}): ${detail}`);
}
const contentType = response.headers.get("content-type") || "";
const result = contentType.includes("application/json")
? await response.json()
: await response.text();
For an API requiring raw image bytes, use the blob directly as the request body and set the content type the API specifies:
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "image/png",
Authorization: `Bearer ${token}`
},
body: imageBlob
});
For an API requiring JSON/base64, convert the blob to base64 and use the exact JSON property it specifies. Base64 increases payload size compared with sending binary, so use it only when the endpoint requires it.
6. Visible viewport, full page, and edge cases
- Only the visible region:
captureVisibleTabdoes not capture content below the fold. Full-page capture needs a separate design, such as scrolling and stitching, and must account for sticky elements, lazy-loaded content, page changes during scrolling, and browser limits. - Active tab matters: capture the intended window’s active tab after the user’s action. Avoid switching tabs between identifying the target and capturing it.
- Capture rate: Chrome’s documented limit is two calls per second. Queue or debounce rapid requests and show a useful message when limiting captures.
- Large images: high-resolution pages can produce large uploads. Check the API’s body-size limit and handle its rejection. If supported, JPEG may reduce payload size for photographic content; PNG is useful when lossless detail matters.
- Service worker lifecycle: service workers can stop when idle. Do not assume timers or in-memory values survive; start work from the event and persist only state that must outlive it.
- Capture privacy: the image reflects what is visible in the tab, which may include account details, messages, or other sensitive data.
7. Test the integration and troubleshoot failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Capture permission error | Missing activeTab or <all_urls>, or capture called for the wrong window context. |
Declare the appropriate capture permission and pass the intended window ID. Trigger capture through the expected user flow. |
| Fetch fails or reports a CORS-like error | API host permission is missing, or the request runs in a content script. | Add the narrow API origin under host_permissions; move the fetch into the service worker or extension page. |
| Server returns 400 or 415 | Wrong multipart field, body format, or image content type. | Compare the request with the API contract. Use the required field and accepted format; do not manually set multipart content type. |
| Server returns 401 or 403 | Missing, invalid, expired, or incorrectly formatted credentials. | Check the required auth scheme and token lifecycle. Do not log the credential while debugging. |
| Server returns 413 or rejects a large body | Image exceeds the endpoint’s payload limit. | Check documented limits and use a supported smaller format or capture strategy. |
| Response parsing throws | Endpoint returns non-JSON or no response body. | Check status and content type, then parse according to the endpoint’s response documentation. |
| Capture calls are throttled | Requests exceed Chrome’s two-calls-per-second documented limit. | Throttle, queue, or debounce capture actions. |
| Image misses below-the-fold content | The API captures only the visible tab viewport. | Use a separately designed and tested full-page method if full-page output is required. |
Inspect the extension service worker console and the API’s documented error response. Avoid printing the screenshot blob or authorization header into logs.
8. Performance, reliability, and cost
Capture and upload are separate costs in time and resource use: the first produces image bytes locally, and the second transfers those bytes and waits for the server. Keep screenshots no larger than the task requires, avoid duplicate captures, and respect Chrome’s capture rate limit. Use the endpoint’s documented timeout, payload limit, and retry guidance; do not blindly retry non-idempotent uploads, since that can create duplicate records or charges. If the API supports an idempotency key, follow its documented behavior.
For reliability, surface progress and errors to the user, distinguish capture failures from HTTP failures, and avoid depending on an open tab or service worker memory after the event completes. Cost depends on the receiving API’s pricing and whether it bills for failed or repeated uploads; check its terms rather than assuming a screenshot capture is free.
Or skip the browser setup
If your goal is to get a clean screenshot of a web page rather than capture the user’s current tab, ScreenshotNeo provides a screenshot API and MCP server. A single request takes a URL and returns a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for parameters and response details.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
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(`ScreenshotNeo returned HTTP ${res.status}`);
await Bun.write("shot.webp", res);
These examples capture a URL on ScreenshotNeo’s service; they do not upload a locally captured browser tab. Cookie banners, popups, and chat widgets are removed before the shot, with each cleanup step configurable. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
FAQ
Can I call the screenshot API directly from a content script?
For cross-origin requests, use the extension service worker or an extension page with host permission for the API. Content scripts remain governed by page-origin restrictions.
Does captureVisibleTab create a file on disk?
No. It returns an image data URL. Convert it to a blob for binary upload, or use another representation only if the server requires it.
Can this capture a page without user interaction?
The example uses a toolbar action and activeTab. A different automated workflow may need different permissions and user-facing behavior; choose permissions according to Chrome’s documentation and your extension’s purpose.
Does ScreenshotNeo upload the screenshot from my open tab?
No. The shown ScreenshotNeo call captures a URL using its API. Use the extension flow above when the image must represent the user’s current visible tab.


