Why Google Apps Script Screenshots Fail and How to Fix Them
Fix blank Apps Script screenshots, authorization errors, deployment identity issues, expired image URLs, and Sheets or Slides capture failures.
Short answer: Google Apps Script screenshots usually fail for one of four separate reasons: the script lacks an OAuth scope, the web app runs as the wrong identity, browser cookies or origin rules block authentication, or the code tries to capture a dynamic UI instead of creating an image blob. Fix the relevant class of failure first. For charts and known images, generate a blob server-side. Use a browser screenshot only when the target is genuinely a rendered web page.
This guide covers authorization, web-app deployments, browser authentication, Sheets and Slides image URLs, remote resources, complete Apps Script examples, diagnostics, limits, and a browser-free ScreenshotNeo option.
1. Identify which failure you have
| Symptom | Likely cause | First fix |
|---|---|---|
| Authorization required or consent never appears | Missing, changed, revoked, or denied OAuth scope | Run a normal function manually in the Apps Script editor and complete authorization. |
/dev works but deployed URL fails |
Different deployment or execution identity | Test the deployed URL and verify execute-as and access settings. |
| OAuth popup is blank or loops | Origin mismatch, blocked third-party cookies, storage policy, or wrong account | Check the exact origin and retry with cookies enabled and one signed-in account. |
| Screenshot shows a loader, iframe, or blank canvas | Browser timing or authenticated UI capture | Export the source as a blob with Apps Script APIs. |
| Image URL works once, then stops | Requester-scoped URL expired or sharing changed | Fetch while authorized and persist a blob or Drive file. |
Sheets insertImage fails |
URL is private or blob exceeds 2 MB | Use a public URL or a compressed blob below the documented limit. |
2. Fix authorization and OAuth scopes
Apps Script scans your project to determine required scopes. Adding a service, changing code, revoking access, or denying a granular permission can leave the current grant incomplete. Google states that an authorization dialog appears when a script needs authorization (authorization guide).
- Save the project.
- Choose a normal function such as
captureRemoteImagein the editor. - Click Run, review the requested scopes, and allow them.
- Run the function again, then retry the web app or trigger.
Installable triggers cannot display an interactive consent dialog. Authorize the project as the user who created the trigger, then run the trigger again. If a Workspace administrator blocks Apps Script, Drive, or external services, ask the administrator to review domain policies.
Minimal authorization test
function authorizationTest() {
const response = UrlFetchApp.fetch('https://example.com');
Logger.log(response.getResponseCode());
}
Running this function manually forces the editor to reveal the current authorization state. A successful response proves only that UrlFetchApp is authorized; Drive, Sheets, Slides, or other services may require additional scopes.
3. Check web-app deployment identity
A web app can execute as the accessing user or as the deploying owner. Those identities may have different Drive, Sheets, Slides, and external-resource access. The /dev URL always uses the latest saved code and is restricted to users with edit access; it is intended for development testing (web-app guide).
- Test the deployed URL, not only
/dev. - Open Deploy > Manage deployments and confirm the active version.
- Check Execute the app as and Who has access.
- Ensure the execution identity can read every source file and image.
- Deploy a new version after code or manifest changes.
If the owner sees an image but another user sees a blank result, compare the identities and sharing permissions. A deployment that runs as the owner can read private files the visitor cannot; a deployment that runs as the visitor needs that visitor’s access.
4. Resolve browser authentication failures
Some failures happen before your script runs. Google documents origin_mismatch when the browser host or port differs from the OAuth client’s registered JavaScript origin, and idpiframe_initialization_failed when third-party cookies or storage are blocked (OAuth browser troubleshooting).
- Match the browser origin exactly, including scheme, host, and port.
- Permit third-party cookies and site storage for Google sign-in, or add the documented exception for
accounts.google.com. - Retry in a clean browser profile with one Google account.
- Check Workspace policies if sign-in, Apps Script, Drive, or external requests are restricted.
Do not treat a fixed consent popup as proof that deployment permissions or image URLs are correct; these are separate failure classes.
5. Prefer server-side blobs over UI screenshots
The Apps Script editor and web-app UI are authenticated, dynamic surfaces. A browser capture can run before content loads, capture an iframe shell, or miss a canvas. When the source is a chart or known image object, create the image on the server.
Export a Sheets chart as PNG
function exportChartToDrive() {
const spreadsheet = SpreadsheetApp.openById('SPREADSHEET_ID');
const sheet = spreadsheet.getSheetByName('Dashboard');
const chart = sheet.getCharts()[0];
if (!chart) throw new Error('No chart found');
const blob = chart.getAs('image/png').setName('dashboard.png');
const file = DriveApp.createFile(blob);
Logger.log(file.getUrl());
}
The chart reference documents getAs(contentType) as returning chart data converted to the requested content type (Chart.getAs).
Export a Slides image
function exportSlideImage() {
const presentation = SlidesApp.openById('PRESENTATION_ID');
const slide = presentation.getSlides()[0];
const image = slide.getImages()[0];
if (!image) throw new Error('No image found');
const blob = image.getAs('image/png').setName('slide-image.png');
DriveApp.createFile(blob);
}
Slides image objects expose blob methods such as getBlob() and getAs() (Slides Image reference).
Fetch a remote image and inspect the response
function fetchRemoteImage() {
const url = 'https://example.com/image.png';
const response = UrlFetchApp.fetch(url, {muteHttpExceptions: true});
const status = response.getResponseCode();
const type = response.getHeaders()['Content-Type'] || '';
if (status < 200 || status >= 300) {
throw new Error('Image request returned HTTP ' + status);
}
if (!type.toLowerCase().startsWith('image/')) {
throw new Error('Expected an image, received ' + type);
}
const blob = response.getBlob().setName('remote-image.png');
DriveApp.createFile(blob);
}
UrlFetchApp lets you fetch the resource, inspect the HTTP status, and then read the response blob. Always check status and MIME type before inserting bytes.
6. Sheets insertion: URL versus blob
Sheets URL insertion has stricter requirements than blob insertion. A URL source must be publicly accessible to the execution context. Blob insertion supports private content but has a documented 2 MB maximum (Sheet.insertImage).
function insertPrivateImage() {
const response = UrlFetchApp.fetch('https://example.com/image.png');
const blob = response.getBlob();
if (blob.getBytes().length > 2 * 1024 * 1024) {
throw new Error('Compress or resize the image below 2 MB');
}
const sheet = SpreadsheetApp.getActiveSheet();
sheet.insertImage(blob, 1, 1);
}
Use URL insertion only for a genuinely public, stable URL. For authenticated or temporary content, fetch the bytes while authorized and insert the blob.
7. Avoid expired Slides and Sheets content URLs
SlidesApp and Sheets cell-image content URLs can be requester-tagged and expire after a short period. They may work in one browser session and fail in Apps Script, another account, or later in the day. Sharing changes can also invalidate access.
Use this pattern instead:
- Request the content while the script is authorized.
- Convert it to a blob.
- Persist the blob in Drive or another controlled store.
- Regenerate the source when needed instead of publishing the temporary URL as a permanent asset.
8. Complete Apps Script web-app example
function doGet() {
return HtmlService.createHtmlOutput(
'<button onclick="google.script.run.withSuccessHandler(show).captureRemoteImage()">Capture</button>' +
'<pre id="result"></pre>' +
'<script>function show(value){document.getElementById("result").textContent=value;}</script>'
);
}
function captureRemoteImage() {
const url = 'https://example.com/image.png';
const response = UrlFetchApp.fetch(url, {muteHttpExceptions: true});
const status = response.getResponseCode();
if (status !== 200) throw new Error('HTTP ' + status);
const blob = response.getBlob();
const contentType = blob.getContentType() || '';
if (!contentType.startsWith('image/')) {
throw new Error('Unexpected content type: ' + contentType);
}
const file = DriveApp.createFile(blob.setName('capture.png'));
return file.getUrl();
}
Authorize UrlFetchApp and DriveApp manually before opening the web app. Deploy a version with the intended execute-as identity, then test the deployed URL.
9. When a browser screenshot is actually appropriate
Use a browser capture when the target is an arbitrary rendered page whose final appearance depends on JavaScript, CSS, fonts, lazy loading, or user interaction and there is no export API. Make the capture deterministic:
- Wait for a specific selector or network idle instead of relying only on a fixed delay.
- Use a stable viewport and timezone.
- Authenticate with headers or cookies where permitted.
- Hide consent banners, chat widgets, and transient overlays.
- Capture an element when a full-page image is unnecessary.
10. Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF. Cookie and consent banners are accepted and 60+ known consent platforms, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing result. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options.
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)
r.raise_for_status()
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(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Features include full-page capture with lazy images loaded, CSS-element capture, dark mode, device presets, retina scale, PDF paper and margin controls, custom CSS and JavaScript, clicks, selector hiding, selector or network-idle waits, request blocking, headers, cookies, user agents, Authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, async jobs with signed webhooks, bulk capture for 100 URLs per call, usage API, and OpenAPI support.
Free includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free. Create a free ScreenshotNeo account.
11. Performance, reliability, and cost notes
- Server-side blobs avoid browser startup, rendering races, and iframe timing.
- For remote resources, inspect status and content type before processing bytes.
- Cache stable captures when freshness allows; avoid repeatedly fetching expiring authenticated URLs.
- Resize or compress images before Sheets blob insertion to stay under 2 MB.
- For browser captures, wait on page state rather than adding an unnecessarily long fixed delay.
- With ScreenshotNeo, cache hits and failed or unusable pages are not billed; inspect
X-Page-VerdictandX-Billedfor each response.
12. Troubleshooting checklist
- Run manually first. This surfaces missing scopes.
- Check execution identity. Compare owner and visitor access to every source.
- Test the deployed URL. Do not use
/devas production evidence. - Inspect browser policy. Verify origin, cookies, storage, and account context.
- Log HTTP status and MIME type. A 200 response can still be HTML or a login page.
- Replace temporary URLs with blobs. Regenerate requester-scoped content when needed.
- Check image size. Keep Sheets blobs below 2 MB.
- Separate failure classes. Fixing OAuth does not fix deployment permissions or expired URLs.
13. FAQ
Why is my screenshot blank only for other users?
The deployment probably executes as the accessing user, or the deployed identity cannot read the source. Check execute-as settings and sharing.
Can I make a Slides content URL permanent?
Treat it as temporary. Fetch the image while authorized and persist a blob or regenerate the URL.
Should I use a URL or blob in Sheets?
Use a URL only when it is public and stable. Use a blob for private content, while staying below the 2 MB limit.
When should I use ScreenshotNeo?
Use it for arbitrary rendered pages when you want a single API request, cleaned captures, usage-aware billing, or MCP access for AI agents.


