How to Capture Grafana Dashboard Screenshots with the API
Automate Grafana PNG exports with Image Renderer, service accounts, render URLs, troubleshooting, and a managed ScreenshotNeo option.
To capture a Grafana view as an image, use Grafana’s server-side Image Renderer and call a render URL with a service-account token. The renderer returns an image such as PNG. For a single panel, the documented route uses /render/d-solo/ plus panelId; verify the dashboard-wide route for your Grafana version before treating it as universal.
Grafana also has a manual Export > Export as image workflow. That is useful for a one-off export, while the renderer is the repeatable path for reports, scheduled jobs, and CI pipelines.
1. Choose the right Grafana workflow
| Need | Use | Important detail |
|---|---|---|
| One visual export | Dashboard UI export | Open the dashboard, choose Export > Export as image, preview it, then download the PNG. |
| Automated panel image | Image Renderer render URL | The official example is a single-panel /render/d-solo/ request with a panel ID. |
| Shareable dashboard snapshot | Snapshot API | This creates a dashboard snapshot object, not an image file, and requires a complete dashboard model. |
| Managed screenshot capture without browser setup | ScreenshotNeo | It accepts a URL and returns PNG, JPEG, WebP, or PDF. |
Do not confuse a rendered PNG with Grafana’s Snapshot API. Grafana describes the legacy snapshot endpoint as designed for the UI, and its API documentation notes migration from /api routes toward /apis in Grafana 13. Choose a render when you need an image artifact; choose a snapshot when you need Grafana’s shareable snapshot format.
2. Check your Grafana deployment and version
- Image rendering is documented for Grafana OSS, Enterprise, and Cloud. Some capabilities depend on the edition.
- The
latestdocumentation is rolling. Check the documentation for the version you run before copying a route or configuration. - On self-managed Grafana, you configure and operate the renderer service. Grafana Cloud manages that infrastructure.
- The renderer guide lists a running Grafana instance, Docker or Linux/Windows binaries, and at least 16 GiB of memory and 4 CPU cores for the renderer service. macOS binaries are not supported; Docker Desktop is recommended on macOS.
For self-managed installations, follow Grafana’s Image Renderer setup guide for the exact configuration keys and version-specific deployment instructions.
3. Configure the Image Renderer service
- Run the Grafana Image Renderer service beside your Grafana instance.
- Configure Grafana with the renderer service URL.
- Configure a callback URL that the renderer can reach. In containerized deployments,
localhostinside the renderer container usually does not mean the Grafana container. - Set a renderer authentication token on the service and the matching token in Grafana. The documented default token is
-; choose a non-default secret for a production deployment. - Keep the renderer endpoint private or protected by network policy and authentication.
Grafana’s Docker example covers the callback and token arrangement. Copy the configuration names from the documentation for your Grafana release rather than assuming values from another version.
Resource and container guidance
Chromium uses memory in addition to the Go process. For a constrained container, Grafana recommends setting GOMEMLIMIT below the container memory limit, with a guideline of 1 GiB of GOMEMLIMIT per 8 GiB of container memory. Treat this as setup guidance, not a throughput benchmark.
4. Create a least-privilege service account
Grafana recommends service accounts for applications that call its HTTP API. Create a service account with permission to view the target folder and dashboard, then create a token for it. Send that token as a Bearer token. Tokens inherit the service account’s permissions; Enterprise supports more granular RBAC.
Keep the token out of URLs, source control, browser JavaScript, and logs. Store it in a secret manager or an environment variable and rotate it when staff, jobs, or deployment boundaries change. See Grafana’s service account documentation for the current UI and API details.
5. Render a Grafana panel with the API
This documented public example renders one panel. Replace the host, dashboard identifier, panel ID, and time range with values from your deployment:
https://play.grafana.org/render/d-solo/ktMs4D6Mk?from=2024-09-03T11:55:44.442Z&to=2024-09-03T17:55:44.442Z&panelId=panel-13&width=1000&height=500&tz=UTC
The example demonstrates these parameters:
| Parameter | Purpose |
|---|---|
from, to |
Time range for the panel query, commonly ISO 8601 timestamps. |
panelId |
The panel to render in the d-solo view. |
width, height |
Output dimensions. Grafana documents 1000 px width and 500 px height as defaults/minimums for panel image rendering. |
tz |
Timezone used for the rendered view, such as UTC. |
scale |
Image scale; the documented default is 1. |
timeout |
Render timeout. Grafana documents a 30-second default that can be increased for slow panel queries. |
cURL
export GRAFANA_URL="https://grafana.example.com"
export GRAFANA_TOKEN="$GRAFANA_SERVICE_ACCOUNT_TOKEN"
curl --fail --location \
--header "Authorization: Bearer ${GRAFANA_TOKEN}" \
--output grafana-panel.png \
"${GRAFANA_URL}/render/d-solo/abc123/dashboard-name?from=now-6h&to=now&panelId=13&width=1000&height=500&tz=UTC"
Python
import os
from pathlib import Path
import requests
base = os.environ["GRAFANA_URL"].rstrip("/")
token = os.environ["GRAFANA_SERVICE_ACCOUNT_TOKEN"]
url = f"{base}/render/d-solo/abc123/dashboard-name"
params = {
"from": "now-6h",
"to": "now",
"panelId": "13",
"width": "1000",
"height": "500",
"tz": "UTC",
"timeout": "60",
}
response = requests.get(
url,
params=params,
headers={"Authorization": f"Bearer {token}"},
timeout=90,
)
response.raise_for_status()
Path("grafana-panel.png").write_bytes(response.content)
print(f"saved {len(response.content)} bytes")
Node.js
const fs = require('node:fs/promises');
const base = process.env.GRAFANA_URL.replace(/\/$/, '');
const token = process.env.GRAFANA_SERVICE_ACCOUNT_TOKEN;
const params = new URLSearchParams({
from: 'now-6h',
to: 'now',
panelId: '13',
width: '1000',
height: '500',
tz: 'UTC',
timeout: '60'
});
const res = await fetch(`${base}/render/d-solo/abc123/dashboard-name?${params}`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) throw new Error(`Grafana returned ${res.status}`);
await fs.writeFile('grafana-panel.png', Buffer.from(await res.arrayBuffer()));
6. Render a dashboard instead of one panel
The cited Grafana route is specifically a single-panel example. A dashboard-wide render may use a different route or parameters depending on your Grafana version and deployment. Confirm the dashboard route in the sharing and image-rendering documentation for that version before putting it into a production job.
When validating a dashboard-wide export:
- Open the dashboard in the browser and record its UID and folder permissions.
- Confirm the renderer can query every panel’s data source.
- Start with a short time range and modest dimensions.
- Compare the image with the browser view at the same timezone and refresh state.
- Only then schedule the request or put it behind a report endpoint.
7. Save and verify the response
The successful response is binary image data. Save it as PNG and inspect the HTTP status and content type before writing the file. Do not silently save an HTML login page or JSON error body with a .png extension.
curl -sS -D response.headers \
--header "Authorization: Bearer ${GRAFANA_TOKEN}" \
"${GRAFANA_URL}/render/d-solo/abc123/dashboard-name?from=now-1h&to=now&panelId=13" \
-o panel.png
file panel.png
cat response.headers
The generated image reflects how the dashboard appears in the browser. Browser resizing, zoom, dashboard changes, and panel state can change the output. Grafana also documents rendering to PDF and CSV through the Image Renderer service, but those are different output formats from a PNG screenshot.
8. Make scheduled captures reliable
- Use explicit time ranges. Absolute UTC timestamps make archived reports reproducible. Relative ranges such as
now-6hare convenient for current dashboards but change on every run. - Allow for query time. Increase the render timeout above the 30-second default when the panel’s data source is slow, while keeping the client timeout longer than the renderer timeout.
- Retry selectively. Retry transient 5xx responses and network failures with exponential backoff. Do not retry a 401, 403, malformed route, or invalid panel ID without changing the request.
- Make jobs idempotent. Include the dashboard UID, panel ID, time range, and render version in the output key so a retry replaces the same artifact.
- Monitor the renderer. The service exposes
/metrics; Grafana identifies Prometheus or Grafana Mimir for metrics and Grafana Tempo as an OpenTelemetry-compatible tracing backend. - Protect credentials. Use a dedicated service account and avoid logging authorization headers or complete URLs containing secrets.
9. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 Unauthorized | Missing, expired, or malformed Bearer token. | Send Authorization: Bearer TOKEN and verify the token belongs to an active service account. |
| 403 Forbidden | The service account cannot view the folder, dashboard, panel, or data source. | Grant the minimum required viewer permissions and confirm folder inheritance. |
| 404 Not Found | Wrong Grafana base path, dashboard UID, route, or panel ID. | Check the deployed version and use the documented route for that version. Confirm the panel ID in the dashboard JSON or UI. |
| Renderer connection error | Grafana cannot reach the renderer, or the callback URL points to an unreachable hostname. | Test connectivity from both containers, use a network-resolvable callback address, and verify the renderer URL and matching token. |
| Timeout after 30 seconds | Panel queries, transformations, or data sources take longer than the default. | Raise the render timeout, reduce the time range, optimize the query, or render fewer panels. |
| Blank or partially loaded image | Data source failure, renderer memory pressure, or capture before panels finish loading. | Check Grafana logs and data-source health, increase resources, and allow more render time. |
| Image file contains JSON or HTML | The client saved an error response without checking status or content type. | Use --fail/raise_for_status(), inspect headers, and log the response body only after redacting secrets. |
| Different time or labels than the browser | Timezone, dashboard variables, refresh state, or browser dimensions differ. | Set tz explicitly, pass the same variables, and compare at the same time range and dimensions. |
| Renderer crashes in a small container | Chromium needs memory beyond the Go process. | Increase the container limit and set GOMEMLIMIT below that limit using Grafana’s memory guidance. |
10. Performance, cost, and operational trade-offs
Rendering cost is mainly query time, browser work, and image size. Smaller time ranges, fewer panels, and appropriate dimensions reduce work. Cache completed artifacts in your own storage when the same time range is requested repeatedly. Avoid parallel bursts that exceed the renderer’s CPU and memory capacity.
Grafana’s documented 1000 × 500 dimensions are defaults/minimums for panel rendering, not a promise of throughput. Measure your own dashboards if you need a schedule or service-level target. The Image Renderer documentation also supports PNG, PDF, and CSV output and describes metrics and tracing integrations for operations.
11. Or skip the browser setup
If you only need a clean image from a Grafana URL, ScreenshotNeo provides a managed screenshot API. See the ScreenshotNeo API documentation for request options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://grafana.example.com/d/abc123/dashboard -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://grafana.example.com/d/abc123/dashboard"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://grafana.example.com/d/abc123/dashboard' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
12. FAQ
Does Grafana’s render URL require a browser session?
No browser session is required when the request is authenticated correctly and the renderer is configured. Use a service-account Bearer token for automation.
Can I use the Snapshot API to get a PNG?
No. The Snapshot API creates a shareable dashboard snapshot with a dashboard model and snapshot data. Use Image Renderer when the required artifact is an image.
What is the safest default timezone?
Use UTC for scheduled archives unless the report is explicitly defined in another timezone. Pass tz and use absolute from/to timestamps when repeatability matters.
Can Grafana render PDFs as well as PNGs?
Yes. Grafana documents PNG, PDF, and CSV rendering through the Image Renderer service; use the output and route supported by your Grafana version.
Why does the image differ from what I see?
The generated image follows the dashboard’s browser appearance. Timezone, zoom, resizing, variables, data freshness, and dashboard changes can all affect the result.
Where should I look for version changes?
Start with Grafana’s current Image Renderer, sharing, service-account, and Snapshot API documentation. The latest docs change over time, and Grafana is migrating API routes.


