Most chart libraries assume a browser that runs JavaScript. Email clients don't give you that. If you want a chart in a weekly digest, you need something that renders without a script tag, or a step that turns a chart into an image before it ever reaches an inbox.
That's the problem ProvChart's API is built for. It returns HTML and CSS, generated server-side, and you decide from there whether to rasterize it into a PNG or embed a tightly controlled snippet. This post walks through the flow, the chart types that actually work in a digest, and a handful of practical tips for running this in production.
Why email is different
The inbox never needs to see your API key or your chart library. It only needs a static image and a link back to the live dashboard. Everything else, the data pull, the payload mapping, the API call, happens on your server or in a worker, before the send.
The flow
- Pull analytics for the period, from your API, warehouse, or database
- Map the rows into a ProvChart payload (
type,series,axisX,theme) -
POST /api/v1/generatewith your API key - Turn the returned
htmlandcssinto a PNG (or store the snippet) - Embed the result in your email template with a CTA back to the dashboard
Mapping analytics to a payload
Keep a single mapper function so every report type shares the same shape:
function analyticsToPayload(report) {
return {
type: "line",
series: [
{
name: "Revenue",
color: "#8b7bff",
points: report.revenueByDay.map(d => d.value)
},
{
name: "Orders",
color: "#4fd8c4",
points: report.ordersByDay.map(d => d.value)
}
],
axisX: report.revenueByDay.map(d => d.label),
theme: {
bg: "#0c0a16",
surface: "#1a1628",
text: "#eae7f5",
muted: "#837da0",
grid: "#8b7bff",
radius: "12px"
}
};
}
Swap the data source underneath it, but keep the function stable. Weekly, monthly, and alert emails all stay consistent this way.
Chart types that actually work in a digest
| Use case | Type | Why |
|---|---|---|
| Trend over 7–30 days |
line / area
|
Readable as a simple story |
| Channel or category mix | bar |
Clear comparison |
| Goal or conversion | gauge |
One number, high impact |
| Share of total | stackedbar |
Good for mix breakdowns |
Skip dense scatter plots or multi-combo charts in email. Save those for the product UI, where someone can actually zoom in and interact.
Ten things worth doing
- Generate server-side. The send job or worker owns the API key, the payload, and the output. Never call the API from the client.
-
Prefer images for email clients. The reliable path is: analytics → API → html/css → headless render (Playwright or Puppeteer) → PNG → CDN →
<img>in the template. This works across Gmail, Outlook, and Apple Mail. - Design for one glance. Each chart should answer one question: did revenue rise, which channel led. Cap it at one or two series and 7–14 data points. Put the headline number in the subject line or preheader.
- Cache by payload hash. Identical type, series, labels, and theme should produce identical output. Cache the html/css or the PNG so repeat sends don't burn quota.
-
Check usage before bulk sends. Hit
GET /api/v1/usagefirst. Ifremainingis low, drop the optional charts or fall back to number-only blocks rather than hittingMONTHLY_LIMIT_REACHEDmid-campaign. -
Theme for email, not the admin UI. Use higher contrast and a background that matches your template. A dedicated
themeobject for digests versus in-app charts goes a long way. - Always add a CTA. A simple "Full dashboard →" link under the chart. The email is the snapshot; the app is where people explore.
- Keep one layout per digest type. Hero metric, one trend chart, one breakdown, a few bullets, then the CTA. Change the data, not the structure.
- Validate before you generate. Empty series, non-numeric points, or oversized arrays waste generations. Enforce max points and required fields in the mapper itself.
- Proxy the key. Production frontends shouldn't hold the secret key at all. Route browser requests through your own backend before they reach ProvChart.
A minimal server-side example
const payload = analyticsToPayload(weeklyReport);
const res = await fetch("https://provchart-api.devtem.org/api/v1/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.PROVCHART_API_KEY
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (!data.success) throw new Error(data.error);
// Option A: rasterize data.html + data.css into a PNG, upload it, then <img src="...">
// Option B: store the snippet for a web version of the same report
Do / Don't
Do
- Generate server-side
- Map analytics to a fixed payload shape
- Cache by hash
- Prefer line, bar, or gauge charts
- Link back to the live dashboard
Don't
- Call the API from the inbox
- Build ad-hoc JSON for every send
- Regenerate identical charts
- Pack five dense series into a digest
- Expect full interactivity in email

Top comments (0)