Chart images for emails and PDF reports

A chart in a dashboard can be interactive. A chart in an email or a PDF has to be a picture, and it has to still be there when someone opens the message next week.

Browser dashboards draw charts with JavaScript. Email clients and PDF files do not run JavaScript, so the weekly summary in someone's inbox and the monthly report attached to it need the chart as an image. The Chart Generator renders that image from labels and numbers. The hard parts are around it: making sure the numbers you send are the numbers you meant, picking a chart the reader can actually read at 500 pixels wide, and getting the image to appear in mail clients that block remote images by default.

The request and the response

It is a POST endpoint, 10 credits per call. Two fields are required:

  • labels: an array of strings, the x-axis categories or the pie slices.
  • datasets: an array of { "name": ..., "values": [...] }, one per series.

Optional: type (bar, the default, line, pie, doughnut, radar or polarArea) and title. width, height and format (png, jpeg or svg) are premium; without them you get a 500 by 300 PNG.

Try it liveChart Generator10 credits
POSThttps://api.apiverve.com/v1/chartgenerator
curl -X POST "https://api.apiverve.com/v1/chartgenerator" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"bar","labels":["Jul","Aug","Sep"],"datasets":[{"name":"Orders","values":[120,145,160]},{"name":"Returns","values":[6,9,4]}],"title":"Orders and returns, Q3 2026"}'
Sign in or create a free account to run this with your own key.

Two datasets share the labels: one bar of each colour per month.

The response carries an id, format (returned as ".png", with a leading dot), the chart type, expires in milliseconds and a signed downloadURL. When we called it, the link lasted about 24 hours. That is long enough to download the image or feed it into a PDF render, and nowhere near long enough to put in an email.

Validate before you pay for a wrong chart

The generator is forgiving in a way that will hurt you. We sent three labels with a dataset of only two values: it returned status: ok and a chart. We sent a value of "abc" in a pie chart: status: ok again. In both cases you pay 10 credits and get an image that quietly misrepresents the data. The only thing it reliably rejected was an unknown type.

So the check belongs on your side, and it is short:

const TYPES = new Set(['bar', 'line', 'pie', 'doughnut', 'radar', 'polarArea']);

export function validateChart({ type = 'bar', labels, datasets }) {
  const problems = [];
  if (!TYPES.has(type)) problems.push(`unknown type ${type}`);
  if (!Array.isArray(labels) || labels.length === 0) problems.push('labels must be a non-empty array');
  if (!Array.isArray(datasets) || datasets.length === 0) problems.push('datasets must be a non-empty array');
  for (const ds of datasets ?? []) {
    if (!Array.isArray(ds.values) || ds.values.length !== labels?.length) {
      problems.push(`dataset "${ds.name}" has ${ds.values?.length ?? 0} values for ${labels?.length ?? 0} labels`);
    } else if (!ds.values.every((v) => typeof v === 'number' && Number.isFinite(v))) {
      problems.push(`dataset "${ds.name}" contains a non-numeric value`);
    }
  }
  if (['pie', 'doughnut'].includes(type) && datasets?.length > 1) problems.push(`${type} charts show one dataset`);
  if (problems.length) throw new Error(`Chart rejected: ${problems.join('; ')}`);
}

export async function renderChart(spec) {
  validateChart(spec);
  const res = await fetch('https://api.apiverve.com/v1/chartgenerator', {
    method: 'POST',
    headers: { 'x-api-key': process.env.APIVERVE_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify(spec),
    signal: AbortSignal.timeout(20000),
  });
  const json = await res.json().catch(() => null);
  if (json?.status !== 'ok') throw new Error(json?.error || `HTTP ${res.status}`);
  const img = await fetch(json.data.downloadURL, { signal: AbortSignal.timeout(20000) }); // no API key
  if (!img.ok) throw new Error(`Chart download failed: HTTP ${img.status}`);
  return Buffer.from(await img.arrayBuffer());
}

The most common way to trip this check in real code is a missing period. Sales by month from a SQL GROUP BY simply has no row for a month with no sales, so the labels come from a calendar and the values from the query, and they disagree by one. Fill gaps with an explicit zero before building the chart; a zero bar tells the truth, a shifted series does not.

Check yourself

Your monthly report shows twelve month labels but the query returned eleven rows because March had no sales. What does the Chart Generator do if you send the eleven values unchanged?

Answer the check above to continue.

Run every example on your own key. Free plan, no card.

Scaling up?

Volume pricing, custom SLAs, and dedicated support for high-traffic teams.

Contact sales