Dashboard Screenshot API
Most BI tools can't export a dashboard as an image in a way that actually works for automation. Some hide export behind an expensive license, others require a separate server product, and a few don't offer an image export API at all.
The task is usually simple: someone needs a screenshot of a dashboard in Slack, in a client email, or in a compliance archive. No logins, no manual screenshots.
A dashboard screenshot API handles this the same way regardless of platform: point it at the URL, pass authentication, wait for charts to render, get back a PNG or PDF. One endpoint instead of five platform-specific workarounds.
Why dashboard screenshots are harder than regular pages
Two problems show up on dashboards that you rarely hit on marketing pages or blog posts.
First: dashboards live behind login. A headless browser visiting your Grafana URL sees the login form, not the dashboard. The screenshot comes back showing username and password fields. Passing session cookies or Bearer tokens with the API request gets the browser past authentication before the capture starts. The authenticated screenshots guide covers the full setup for cookies, headers, and localStorage-based auth.
Second, and this is the one that catches people off guard: charts render asynchronously. The page shell loads in under a second. Then API calls fire to fetch data. Then Chart.js or D3 or Recharts draws the visualization into a canvas element. A screenshot taken at page load captures empty rectangles where charts should be. I've seen Grafana dashboards with 20+ panels take 8-10 seconds before every panel finishes rendering, especially on Azure Managed Grafana where CPU is shared across tenants.
The fix is wait_for_selector targeting an element that only exists once charts finish drawing. For Grafana, that's .react-grid-layout (the panel container). For custom dashboards built with Recharts, look for a data attribute your app sets after render completes. Fixed delays work too, but a selector-based wait catches the exact moment instead of guessing.
Screenshot a Grafana dashboard with the API
Grafana is the most common dashboard people want to capture programmatically. The community forums are full of threads about the image renderer plugin timing out, panels rendering blank because of lazy loading, and the Enterprise paywall for scheduled PDF reports. Frustrating, especially when all you need is a daily PNG.
One thing worth knowing: Grafana lazy-loads panels by default. Only panels visible in the viewport actually render. Panels below the fold stay blank until you scroll to them. Append &disableLazyLoad=true to the dashboard URL to force all panels to render upfront. Without this, full-page screenshots come back with empty panels at the bottom.
This cURL request captures a full Grafana dashboard with authentication, lazy loading disabled, and a wait for the panel container:
curl "https://api.screenshotrun.com/v1/screenshots/capture" \
-H "Authorization: Bearer YOUR_API_KEY" \
-G \
--data-urlencode "url=https://grafana.yourcompany.com/d/abc123/infrastructure?disableLazyLoad=true" \
--data-urlencode "cookies=[{\"name\":\"grafana_session\",\"value\":\"YOUR_SESSION_TOKEN\",\"domain\":\"grafana.yourcompany.com\"}]" \
--data-urlencode "wait_for_selector=.react-grid-layout" \
--data-urlencode "width=1920" \
--data-urlencode "full_page=true" \
--data-urlencode "format=png" \
--data-urlencode "delay=3" \
-o dashboard.png
That 3-second delay after the selector appears lets time-series panels finish drawing their data points. Most Grafana dashboards complete rendering within that window. Heavier dashboards with 30+ panels or long time ranges might need 5 seconds.
Same request in Node.js, posting the result to a Slack channel:
const params = new URLSearchParams({
url: 'https://grafana.yourcompany.com/d/abc123/infrastructure?disableLazyLoad=true',
cookies: JSON.stringify([
{ name: 'grafana_session', value: process.env.GRAFANA_TOKEN, domain: 'grafana.yourcompany.com' }
]),
wait_for_selector: '.react-grid-layout',
width: '1920',
full_page: 'true',
format: 'png',
delay: '3'
});
const response = await fetch(
`https://api.screenshotrun.com/v1/screenshots/capture?${params}`,
{ headers: { 'Authorization': `Bearer ${process.env.SCREENSHOTRUN_API_KEY}` } }
);
const imageBuffer = Buffer.from(await response.arrayBuffer());
// Post to Slack
await fetch('https://slack.com/api/files.uploadV2', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.SLACK_TOKEN}` },
body: new FormData().append('file', new Blob([imageBuffer]), 'dashboard.png')
.append('channel_id', 'C0123CHANNEL')
.append('title', `Infrastructure Dashboard — ${new Date().toLocaleDateString()}`)
});
Wrap that in a cron job and the #ops channel gets a fresh dashboard snapshot every morning. No Grafana Image Renderer plugin to maintain, no Docker container to keep running. That alone made the switch worth it for one team I helped set this up for. Full parameter reference in the Node.js integration guide.
One dashboard screenshot API for every platform
The same approach works for any dashboard tool that runs in a browser. The specifics change per platform, but the pattern stays the same: URL + auth + wait for charts.
| Platform | Auth method | Wait target | Tips |
|---|---|---|---|
| Grafana | Session cookie or API key via Authorization header |
.react-grid-layout |
Add &disableLazyLoad=true to URL for full-page captures |
| Tableau Server | Trusted ticket or session cookie | .tab-vizHeader |
Use the /views/{workbook}/{view} embed URL, not the portal URL |
| Power BI | Embed token via headers |
.visualContainer |
Use the embed URL from Power BI Embedded, not the app.powerbi.com URL |
| Metabase | Session cookie (metabase.DEVICE) |
.DashCard |
Metabase has no native image export API. This is one of the few ways to automate it. |
| Apache Superset | Session cookie | .dashboard-grid |
Known bug: MachineAuthProvider can clobber session cookies. Pass cookies explicitly. |
| Looker Studio | Google session cookie | .looker-vis-context-title |
Google Auth cookies are complex. Consider a service account with embed access. |
| Kibana | Elastic session cookie or API key | .embPanel |
Set delay=5 for dashboards with heavy Elasticsearch queries |
| Custom (React/Vue) | Your app's auth (cookie or Bearer token) | Your app's render-complete indicator | Add a data-charts-loaded="true" attribute your app sets after all charts render |
For custom dashboards built with Chart.js, D3, or Recharts, add a DOM attribute that your frontend sets once all visualizations finish drawing. Something like document.body.setAttribute('data-dashboard-ready', 'true') after the last chart callback fires. Then pass wait_for_selector=[data-dashboard-ready] to the API. More reliable than any fixed delay because your app knows when it's done rendering. The API doesn't have to guess.
Capture a single widget instead of the full dashboard
Sometimes you don't need the whole dashboard. A weekly email might only need the revenue chart. A Slack alert might only need the error rate panel. The selector parameter captures a specific element instead of the full page:
curl "https://api.screenshotrun.com/v1/screenshots/capture" \
-H "Authorization: Bearer YOUR_API_KEY" \
-G \
--data-urlencode "url=https://grafana.yourcompany.com/d/abc123/infrastructure" \
--data-urlencode "cookies=[{\"name\":\"grafana_session\",\"value\":\"TOKEN\",\"domain\":\"grafana.yourcompany.com\"}]" \
--data-urlencode "selector=.react-grid-item:nth-child(3)" \
--data-urlencode "delay=3" \
--data-urlencode "retina=true" \
-o single-panel.png
Retina mode produces a 2x resolution image (worth knowing: the file size quadruples, not doubles, because both width and height scale). Useful for presentations where the chart gets projected on a large screen or embedded in a high-DPI PDF report. Pick the right output format based on where the image ends up: PNG for Slack, WebP for web embeds, PDF for email attachments.
Automated dashboard screenshots on a schedule
A one-off capture is useful for debugging. What most teams actually build is automated dashboard screenshots that run daily or weekly and deliver the result somewhere stakeholders will see it.
For small volumes (under 10 dashboards), a synchronous cron job works fine. Fire the API request, wait for the response, save or send the image. For anything larger, the webhook_url parameter is the better pattern: the API returns immediately, processes the screenshot in the background, and POSTs the result to your endpoint when it's ready. Your cron job finishes in seconds regardless of how many dashboards it triggers.
Python example that captures three dashboards and exports them as PDF (full setup in the Python integration guide):
import requests
dashboards = [
{"name": "Infrastructure", "url": "https://grafana.company.com/d/infra/overview?disableLazyLoad=true"},
{"name": "Revenue", "url": "https://metabase.company.com/dashboard/42"},
{"name": "Error Rates", "url": "https://grafana.company.com/d/errors/overview?disableLazyLoad=true"},
]
for dash in dashboards:
resp = requests.get(
"https://api.screenshotrun.com/v1/screenshots/capture",
headers={"Authorization": "Bearer YOUR_API_KEY"},
params={
"url": dash["url"],
"cookies": '[{"name":"session","value":"TOKEN","domain":".company.com"}]',
"wait_for_selector": ".react-grid-layout, .DashCard",
"full_page": "true",
"format": "pdf",
"pdf_landscape": "true",
"delay": "3",
},
)
with open(f"{dash['name'].lower().replace(' ', '-')}.pdf", "wb") as f:
f.write(resp.content)
I run a script like this for a client's weekly executive report. The whole pipeline is about 20 lines of code beyond what's shown here: merge the PDFs with PyPDF2, email via SendGrid, done. Compare that to the Puppeteer setup it replaced, which managed Chromium processes, handled cookie refresh logic, and broke roughly every four weeks when Chrome auto-updated.
No-code alternative: Zapier, n8n, and Make.com can all trigger the API on a schedule and route the result to email, Slack, Google Drive, or S3. If your dashboard uses a dark theme, the dark_mode parameter preserves it in the capture.
What won't work well
Dashboards that require multi-step authentication (SSO with SAML redirects, OAuth flows with consent screens, 2FA prompts) can't be handled with a simple cookie or header injection. The API doesn't click through login flows. If your dashboard sits behind Okta or Azure AD with mandatory 2FA, you'll need to generate a pre-authenticated session token on your side and pass that token to the API. Some platforms make this easy (Grafana's API keys, Tableau's trusted tickets). Others don't. Looker Studio with Google Auth is particularly painful to automate.
Dashboard rendering on Linux also looks slightly different from macOS or Windows. Fonts fall back to Liberation Sans instead of Helvetica, and anti-aliasing differs. For most reporting use cases this doesn't matter. But if pixel-perfect brand consistency is critical, inject a custom font via the css parameter: @import url('https://fonts.googleapis.com/css2?family=Inter'); and override the dashboard's font stack.
Start with one dashboard. Pick the one your team checks most often, wire up the API call with auth and a wait_for_selector, and run it on a cron schedule. If the image looks right, add the rest. ScreenshotRun's free tier gives you 200 captures per month to test the workflow before committing.
Frequently asked questions
Yes. Pass a session cookie or API key for authentication, add disableLazyLoad=true to the URL, and use wait_for_selector to ensure all panels render before capture. No Docker container or Enterprise license required.
Set up a cron job that calls the screenshot API with your dashboard URL, authentication credentials, and desired output format. For larger volumes, use the webhook_url parameter for async processing. No-code tools like Zapier and n8n also support scheduled triggers.
Dashboard charts render asynchronously. The page shell loads first, then JavaScript fetches data and draws visualizations. Use wait_for_selector targeting a chart element (like .react-grid-layout for Grafana) combined with a delay of 3-5 seconds to ensure all panels finish rendering.
Set format=pdf and optionally pdf_landscape=true in your API request. The API renders the dashboard in a headless browser and returns a PDF file. Works for Grafana, Metabase, Tableau, Power BI, and custom dashboards.
Pass authentication credentials using the cookies or headers parameter. For Grafana, inject the grafana_session cookie. For Tableau, use a trusted ticket. For custom dashboards, pass your app's session cookie or Bearer token in the Authorization header.
Use the selector parameter to target a specific element. For example, selector=.react-grid-item:nth-child(3) captures only the third panel in a Grafana dashboard. Add retina=true for 2x resolution output suitable for presentations.
Any dashboard accessible via a browser URL works: Grafana, Tableau Server, Power BI, Metabase, Apache Superset, Looker Studio, Kibana, and custom dashboards built with Chart.js, D3, or Recharts.
Vitalii Holben