Automatically Generate Financial Chart Images With an API
Build reliable financial chart images by validating market data, rendering Chart.js with an API, and automating PNG, WebP, SVG, or PDF output.

To automatically generate a financial chart image, separate the job into two systems: obtain and validate market data, then send a Chart.js configuration to an image renderer. QuickChart documents this workflow for URL-based requests and POST requests. The renderer turns your labels, datasets, and options into PNG, JPG, WebP, SVG, PDF, or base64 output; it is not a market-data feed. You must source prices, timestamps, and corporate-action adjustments from a provider appropriate for your instrument and use case.
1. The complete pipeline
A reliable chart image pipeline has five stages:

- Acquire data. Fetch candles, closes, fundamentals, or other observations from your data provider. Record the provider, symbol, timezone, and retrieval time.
- Normalize. Convert timestamps to one timezone, sort ascending, deduplicate, and represent missing observations deliberately. Never silently turn a missing trading day into a zero price.
- Validate. Check that labels and every dataset have the same length, values are finite numbers, and the requested period is the one you intend to publish.
- Describe the chart. Build a Chart.js configuration containing
type,data, and optionaloptions. Keep this object separate from data-fetching code so you can test it with a fixture. - Render and store. POST the configuration to an image endpoint, check HTTP status and content type, then save the returned bytes to durable storage. A generated URL is not automatically permanent; download the bytes when retention matters.
QuickChart documents Chart.js configuration over a URL or POST request and recommends URL encoding for GET requests (API documentation). Its POST endpoint lists PNG, JPG, WebP, SVG, PDF, and base64 output options. Match the Chart.js version requested by the endpoint to the syntax in your configuration.
2. Build a financial chart configuration
This fixture uses daily closing prices. Replace the arrays with validated provider data.
const config = {
type: 'line',
data: {
labels: ['2026-09-21', '2026-09-22', '2026-09-23', '2026-09-24', '2026-09-25'],
datasets: [{
label: 'ACME close (USD)',
data: [101.20, 102.05, 100.90, 103.40, 104.10],
borderColor: '#1769aa',
backgroundColor: 'rgba(23, 105, 170, 0.12)',
fill: true,
pointRadius: 2,
tension: 0.2
}]
},
options: {
plugins: {
title: { display: true, text: 'ACME daily close' },
legend: { display: true }
},
scales: {
x: { title: { display: true, text: 'Date' } },
y: { title: { display: true, text: 'Price (USD)' }, beginAtZero: false }
}
}
};
For OHLC or candlestick displays, use a chart type and plugin supported by the selected Chart.js version. The same data rules apply: prices must be numeric, timestamps ordered, and missing values represented according to the chart semantics. For percentage returns, calculate the return series before rendering and label the axis as a percentage.
3. Render with a POST request
POST is the practical default for generated configurations because the request body avoids URL-length limits.
curl -X POST https://quickchart.io/chart \
-H 'Content-Type: application/json' \
-d '{
"width": 1200,
"height": 675,
"format": "png",
"version": "4",
"chart": {
"type": "line",
"data": {
"labels": ["2026-09-21", "2026-09-22", "2026-09-23", "2026-09-24", "2026-09-25"],
"datasets": [{
"label": "ACME close (USD)",
"data": [101.2, 102.05, 100.9, 103.4, 104.1],
"borderColor": "#1769aa",
"fill": false
}]
},
"options": {
"scales": { "y": { "beginAtZero": false } }
}
}
}' \
-o acme-close.png
Check the response before publishing:
status=$(curl -sS -o acme-close.png -w '%{http_code}' -X POST https://quickchart.io/chart \
-H 'Content-Type: application/json' \
--data @request.json)
[ "$status" = 200 ] || { echo "render failed: HTTP $status" >&2; exit 1; }
file acme-close.png
Put JSON in request.json when shell quoting becomes difficult. Keep credentials for your data provider outside this file and outside logs.
4. GET requests and URL encoding
GET is useful for a small, cacheable chart definition. Encode the configuration rather than concatenating raw JSON.
curl -G 'https://quickchart.io/chart' \
--data-urlencode 'width=1000' \
--data-urlencode 'height=560' \
--data-urlencode 'format=webp' \
--data-urlencode 'chart={"type":"line","data":{"labels":["Mon","Tue","Wed"],"datasets":[{"label":"Close","data":[10,12,11]}]}}' \
-o closes.webp
URL encoding matters for braces, quotes, spaces, and ampersands. Large datasets can exceed proxy or browser URL limits; switch to POST. If you use a QuickChart short URL or template, treat its expiration rules as an operational setting and retain a downloaded image for permanent records (short URL guidance).
5. Python implementation
import math
from pathlib import Path
import requests
labels = ['2026-09-21', '2026-09-22', '2026-09-23', '2026-09-24', '2026-09-25']
closes = [101.20, 102.05, 100.90, 103.40, 104.10]
if len(labels) != len(closes) or not labels:
raise ValueError('labels and closes must have the same non-zero length')
if any(not isinstance(v, (int, float)) or not math.isfinite(v) for v in closes):
raise ValueError('close values must be finite numbers')
payload = {
'width': 1200, 'height': 675, 'format': 'png', 'version': '4',
'chart': {'type': 'line', 'data': {'labels': labels, 'datasets': [{
'label': 'ACME close (USD)', 'data': closes,
'borderColor': '#1769aa', 'fill': False}]},
'options': {'scales': {'y': {'beginAtZero': False}}}}
}
response = requests.post('https://quickchart.io/chart', json=payload, timeout=30)
response.raise_for_status()
content_type = response.headers.get('content-type', '')
if 'image' not in content_type and 'application/pdf' not in content_type:
raise RuntimeError(f'unexpected content type: {content_type}')
Path('acme-close.png').write_bytes(response.content)
6. Node.js implementation
import { writeFile } from 'node:fs/promises';
const labels = ['2026-09-21', '2026-09-22', '2026-09-23', '2026-09-24', '2026-09-25'];
const closes = [101.2, 102.05, 100.9, 103.4, 104.1];
if (labels.length !== closes.length || closes.some(v => !Number.isFinite(v))) throw new Error('invalid financial series');
const body = { width: 1200, height: 675, format: 'png', version: '4', chart: {
type: 'line', data: { labels, datasets: [{ label: 'ACME close (USD)', data: closes, borderColor: '#1769aa', fill: false }] },
options: { scales: { y: { beginAtZero: false } } }
}};
const response = await fetch('https://quickchart.io/chart', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
if (!response.ok) throw new Error(`render failed: HTTP ${response.status}`);
await writeFile('acme-close.png', Buffer.from(await response.arrayBuffer()));
7. Output formats and configuration
| Format | Best use | Note |
|---|---|---|
| PNG | Email, documentation, lossless raster | Broad support. |
| JPG | Bandwidth-sensitive previews | Lossy compression can soften lines and text. |
| WebP | Web delivery | Verify downstream support. |
| SVG | Vector scaling and print | Sanitize where untrusted content is embedded. |
| Reports and print exports | Set dimensions for the final page layout. | |
| Base64 | JSON pipelines | Decode and persist it; payloads are larger. |
Choose width and height from the destination. A social card, dashboard thumbnail, and research appendix need different aspect ratios. For dense series, reduce label density or increase canvas width.
8. Data quality and financial edge cases
- Missing sessions: Keep gaps or forward-fill only when your analytical definition calls for it.
- Splits and dividends: Decide whether the series is adjusted or raw and state that in metadata.
- Timezone boundaries: Convert timestamps before deriving daily labels.
- Outliers: Do not clip genuine moves merely to improve composition.
- Null values: Reject NaN and infinity before serialization.
- Reproducibility: Store input JSON, data snapshot identifier, renderer version, and output hash.
9. Reliability, performance, and cost controls
Cache normalized data for the period you publish, then cache rendered bytes using a key containing symbol, interval, range, chart configuration, output format, and renderer version. Add retries only for transient network failures, with exponential backoff and a small maximum attempt count. Use a queue for batch publication, limit concurrency, record status codes and response times, and alert on unexpected image dimensions or content types.
For immutable reports, save bytes in object storage under your retention policy. For live pages, regenerate on a schedule tied to the data interval and show the data timestamp beside the image. Keep provider and rendering credentials in environment variables or a secret manager. Review QuickChart’s current limits, pricing, and licensing terms before launch; its product page describes AGPLv3 and commercial licensing.
10. Troubleshooting checklist
| Symptom | Cause | Fix |
|---|---|---|
| HTTP 400 | Malformed JSON or unsupported syntax | Validate JSON and align with the requested Chart.js version. |
| Blank image | Empty labels, null values, or hidden dataset | Log normalized arrays and render a three-point fixture. |
| Overlapping dates | Too many labels | Increase width or reduce tick density. |
| Wrong trading day | Late timezone conversion | Convert timestamps before grouping. |
| Unreadable output | Canvas too small | Render at final pixel dimensions. |
| GET fails, POST works | URL length or encoding limit | Use POST. |
| Image disappears | Only an expiring URL was retained | Download and store response bytes. |
| Intermittent timeout | Transient network or service load | Use bounded timeouts and backoff. |
11. When an interactive chart is better
An image suits email, PDFs, social cards, and immutable reports. It cannot provide hover values, zooming, live updates, or accessibility behavior by itself. TradingView describes Lightweight Charts as a library for interactive financial charts (project site). Choose an interactive library when users need browser exploration; choose a renderer when a stable artifact is the deliverable.
12. Or skip the browser setup
If your chart already exists in a web page and you need a clean image of that page or a specific chart element, ScreenshotNeo captures it through one API call. It supports full-page or CSS-selector captures, custom JavaScript and CSS, waits for a selector, delay, or network idle, chosen viewports, device presets, dark mode, and retina scale. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/chart -o chart.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/chart"}, timeout=90)
open("chart.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/chart' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Does a chart image API provide stock prices?
No. QuickChart renders the configuration you send. Obtain and validate market data separately.
Should I use GET or POST?
Use GET for small encoded examples and POST for assembled or large configurations.
Which format is best for email?
PNG is the safest default. Use WebP only when every recipient system supports it.
How do I keep an image permanently?
Save returned bytes in storage you control instead of relying on an expiring short URL.
Can the result be interactive?
No. A rendered image is static; use an interactive financial chart library for hover, zoom, or live updates.


