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.

Capture the page with a browser automation tool such as Playwright, ask your trusted backend to create a presigned upload URL for a specific object, then send the screenshot bytes to that URL with a browser PUT request. Configure the storage bucket’s CORS policy for your exact website origin, the PUT method, and the headers used by the signed request. Never put permanent storage credentials in frontend JavaScript.

How capture and upload fit together

Taking a screenshot and storing it are separate operations. Playwright renders the page and returns screenshot bytes; an object-storage service accepts those bytes. For a browser-based upload, a backend should authorize the specific upload by creating a presigned URL. The browser uses that temporary URL without receiving the bucket’s secret credentials. See the Playwright Page API, Cloudflare R2 presigned URLs, and AWS presigned upload documentation.

Authorization and CORS are different controls. The signature permits an operation on an object; CORS governs whether browser code from a particular origin can make and read a cross-origin request. A valid signature does not bypass the browser’s CORS checks, and a CORS rule does not make a private object publicly readable. Cloudflare’s documentation describes presigned URLs as a way to grant temporary object access without exposing API credentials; it also advises treating them as bearer tokens.

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

Capture a screenshot with Playwright

Run Playwright in a trusted browser-automation process, such as a backend worker. Choose a readiness condition that fits the page: navigation completing does not guarantee that client-rendered content, fonts, or late-loading images are ready. Playwright supports waiting for a selector, application-specific readiness, or an appropriate delay before capture.

The Page API captures the viewport by default. Set fullPage: true for the full scrollable page. You can also capture an element or clipped region, and choose PNG, JPEG, or WebP. Device scale affects output resolution and file size; disabling animations can help make captures more repeatable. If the page may display credentials, personal information, or internal application content, consider masking sensitive elements. These options are documented in the Playwright Page API.

Use a backend-issued presigned URL

  1. Request an upload authorization. The website asks your backend for permission to upload. The backend authenticates and validates the caller, chooses the bucket and object key, and signs a narrowly scoped PUT request. Do not let an untrusted client choose arbitrary keys or access credentials.
  2. Return only what the client needs. The backend returns the presigned URL and any required headers, along with the object key for the application to retain. R2’s upload objects guide documents direct client uploads using presigned URLs; AWS describes the same general approach for presigned uploads.
  3. Capture and upload the bytes. Send the screenshot buffer to the presigned URL using the exact headers used when signing. If the signature includes Content-Type: image/png, send that same content type.
  4. Handle the storage response. Use a successful storage response to determine whether the upload completed. If browser code needs to read response metadata such as ETag, expose that header in the bucket’s CORS configuration. Keep the bucket and object key supplied by your backend as the durable identifier rather than parsing the provider URL.

Here is the browser-side shape of the PUT once your application has a screenshot buffer, a URL from its backend, and the required content type. It assumes the bucket’s CORS policy allows the application origin and request:

const response = await fetch(uploadUrl, {
  method: 'PUT',
  headers: { 'Content-Type': 'image/png' },
  body: screenshotBytes,
});
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);

Adapt the content type and signed headers to match the format actually captured and the backend’s signature. This example does not generate the presigned URL; signing belongs on the trusted server.

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

Configure bucket CORS narrowly

Allow only the origins that need browser access, the methods the browser actually uses (usually PUT for this flow), and the headers sent in the signed request. Add checksum or metadata headers only if the implementation requires them. If the browser must inspect a response header such as ETag, expose it explicitly. Cloudflare’s R2 CORS documentation explains the bucket-side policy; confirm the exact configuration format and behavior with your storage provider.

A CORS policy is not an access-control substitute. It does not grant the signature, make an object public, or prevent a leaked presigned URL from being used by someone else while it remains valid.

Choose one PUT or multipart upload

For ordinary screenshot files, a single PUT is often the simpler choice, but unusually tall pages or high device-scale output can produce larger files, so check actual screenshot sizes. Cloudflare R2’s 2026 upload documentation specifies a 5 GiB maximum for a single upload. It specifies multipart uploads up to 5 TiB across as many as 10,000 parts, with multipart useful for large objects, parallelism, and resumability. These are R2 limits, not universal S3-compatible guarantees; check the chosen provider’s current limits and multipart API.

Choose a capture environment and access model

Browser automation or a hosted capture service

Running Playwright yourself gives you control over the browser version, readiness logic, output, and integration, but you operate the browser infrastructure and concurrency. A hosted browser or screenshot API can reduce that operational work; evaluate its control over rendering, scale, and concurrency against your needs. ScreenshotNeo is a website screenshot API and MCP server for developers. Its clean-shot workflow accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; those steps can be turned off. It bills only clean shots: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, with response headers indicating the page verdict and billing status.

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.

ScreenshotNeo also supports full-page and element captures, image formats, PDF, browser and viewport options, and other capture controls. For uploads, your application still needs a storage destination and an appropriate authorization flow; do not treat a screenshot service as a replacement for storage permissions.

Direct upload or application-server proxy

A direct presigned upload sends bytes from the browser to storage instead of routing them through your application server. Proxying the bytes through your server can centralize checks and application logic, but adds server load and data transfer. Cloudflare documents presigned URLs for direct client uploads in its R2 upload guide.

Private or public objects

Choose access based on who should be able to view a screenshot. Keep objects private and issue signed reads when access should be limited; make them public only when public access is intended. A presigned upload URL authorizes a temporary operation for its holder and is not itself a public-read setting. See Cloudflare R2 CORS documentation for the distinction between browser access rules and object access.

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

Security and troubleshooting

Protect credentials and signed URLs

  • Never embed permanent bucket access keys in website code. Have a trusted backend create the authorization; AWS describes presigned uploads as allowing upload without giving the uploader AWS security credentials.
  • Limit a presigned URL to the intended object and operation, and use the shortest practical lifetime that allows the upload to finish. Cloudflare R2 documents expirations from 1 second to 7 days; that is an R2-specific range, not a universal provider rule.
  • Do not log or publicly share presigned URLs. Anyone holding one can use its authorized operation until it expires.

Browser reports a CORS error

Inspect the browser’s network panel for the preflight request and compare the bucket rule with the exact origin, method, and request headers. A command-line upload can succeed while a browser request fails because CORS is enforced by browsers. Cloudflare notes that an expired R2 presigned URL response does not include CORS headers, so browser JavaScript may be unable to read its error body; renew the URL before expiry or have the application request a replacement.

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

Storage reports a signature mismatch

Compare the signed headers with the actual request. In particular, if the backend included a content type when signing, send exactly that value on the PUT. Cloudflare’s R2 JavaScript SDK example illustrates the signed-header requirement.

Provider-specific behavior differs

“S3-compatible” does not promise identical endpoint formats, region values, form-upload modes, checksums, CORS behavior, or support for every AWS S3 feature. The cited upload sizes and URL expiry range are Cloudflare R2 documentation values, current as of September 29, 2026; check your provider’s own current documentation before relying on them.

Skip the browser setup with ScreenshotNeo

Or skip the browser setup: ScreenshotNeo can return a screenshot from one GET request. Create an API key, then replace the example URL with the page you want to capture. The response is the image; your application can then upload those bytes to S3-compatible storage using the presigned flow above. See the ScreenshotNeo API documentation.

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

ScreenshotNeo’s API also accepts parameter names used by other screenshot APIs, which can simplify a migration. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

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

ScreenshotNeo plans

ScreenshotNeo lists the following monthly allowances and prices. Yearly billing gives two months free; every feature is on every plan.

Plan Price Monthly shots
Free $0 1,000
Starter $5 3,000
Growth $15 15,000
Pro $39 60,000
Scale $99 250,000
Business $249 1,000,000

The Free plan requires no card. See ScreenshotNeo for current plan details.

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.