Saved charts
POST a config, get a stable URL. Embed in emails, Markdown, Slack – no API key in the markup.
You can save a chart config server-side and get back a stable, opaque URL that renders on demand. Embed it as <img src="https://szum.io/c/{id}"> anywhere <img> tags work – emails, Markdown, Slack, Notion – your API key stays out of the markup.
<img src="https://szum.io/c/abc123" />This is the production primitive for embedding. The keyless GET /chart path works too, but it puts the full config in the query string. Saved charts give you opaque short URLs, configs up to 50 KB, and access to custom themes.
When to use which
GET /chart?config=... | Saved chart /c/{id} | |
|---|---|---|
| Auth | Keyless | API key to write, keyless to read |
| URL shape | Full config in query | Short opaque id |
| Config size | URL-length limited | Up to 50 KB |
| Use case | Prototyping, no-auth | Production emails, dashboards |
The chart object
Every JSON endpoint that returns a saved chart – create, list, read-by-id – returns the same shape:
{
"id": "abc123...",
"source": "api",
"title": "Quarterly revenue",
"createdAt": "2024-06-01T00:00:00.000Z",
"updatedAt": "2024-06-01T00:00:00.000Z",
"sizeBytes": 412,
"publishedAt": "2024-06-01T00:00:00.000Z",
"imageUrl": "https://szum.io/c/abc123...",
"embedUrl": "https://szum.io/e/abc123...",
"configUrl": "https://szum.io/api/charts/abc123.../config"
}| Field | Notes |
|---|---|
id | The opaque capability. Holds for /c/, /e/, and the JSON endpoints. |
source | Where the chart was created – currently "api", "figma", "app", or "mcp". Treat it as an open set (match the values you care about; new origins may appear) and as a caller-provided hint on POST, not verified provenance. |
title | An explicit title in the request body if given, else the config's title (empty string if neither). |
createdAt | ISO-8601 timestamp, e.g. 2024-06-01T00:00:00.000Z. |
updatedAt | ISO-8601 timestamp; equals createdAt until you replace the config in place (PUT) or rename (PATCH), then it's bumped. |
sizeBytes | Stored (compressed) size. |
publishedAt | ISO-8601 timestamp or null. null means the public /c/ and /e/ URLs are dark (e.g. an unpublished editor draft). API-created charts are published on create. |
imageUrl | Rendered image (/c/{id}). Add .png / .svg to force a format. |
embedUrl | Interactive HTML embed (/e/{id}). |
configUrl | The chart's config (the definition); owner-only. |
The config, image, and embed are sub-resources you fetch off the chart. The object itself is metadata plus links, so listing thousands of charts never drags their configs along.
API reference
All endpoints use your API key (Authorization: Bearer YOUR_API_KEY) and share the error vocabulary and per-key rate limit.
Create a chart – POST /api/charts
Save a config and receive the chart object (201). Wrap the config in { "config": ... }:
curl -X POST https://szum.io/api/charts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"config":{"version":"2026-03-20","format":"png","marks":[{"type":"barY","data":[{"x":"Q1","y":42}]}]}}'import { Szum } from "@szum-io/sdk";
const szum = new Szum({ apiKey: process.env.SZUM_KEY! });
const chart = await szum.charts.create({
format: "png",
marks: [{ type: "barY", data: [{ x: "Q1", y: 42 }] }],
});
// chart.imageUrl → /c/{id}, chart.embedUrl → /e/{id}Retry-safe creates. Pass an Idempotency-Key header and a retried POST returns the original chart object instead of saving a duplicate – the key is remembered for 24 hours. A second request with the same key while the first is still in flight returns 409 (with Retry-After). Dedup is by key alone (no payload check): reusing a key returns the original chart even if you send a different config, so use a fresh key per distinct chart. The @szum-io/sdk charts.create generates one automatically per call.
List charts – GET /api/charts
List your charts, newest first by default. All query params are optional:
| Param | Default | Notes |
|---|---|---|
source | all | Omit to list every chart, or filter to one or a comma-separated set of figma / api / app / mcp (e.g. ?source=figma,app). |
sort | created | created (newest first – createdAt never changes, so paging stays stable as you edit), updated (recently edited first), or title (A→Z). An unknown value is a 400. |
q | – | Case-insensitive title substring filter. When present, the response also carries total – the exact match count. |
limit | 100 | Page size, max 1000. |
cursor | – | Omit for the first page; then pass the nextCursor from the previous response. The cursor is coupled to the sort it was minted under – switching sorts mid-pagination is a 400. |
# First page of everything
curl "https://szum.io/api/charts" -H "Authorization: Bearer YOUR_API_KEY"
# Just API-created charts, 100 per page
curl "https://szum.io/api/charts?source=api&limit=100" -H "Authorization: Bearer YOUR_API_KEY"Returns a page of chart objects plus a nextCursor that is null on the last page. nextCursor is an opaque token – pass it straight back as ?cursor=:
{
"items": [
{
"id": "abc123...",
"source": "api",
"title": "Quarterly revenue",
"createdAt": "2024-06-01T00:00:00.000Z",
"updatedAt": "2024-06-01T00:00:00.000Z",
"sizeBytes": 412,
"imageUrl": "...",
"embedUrl": "...",
"configUrl": "..."
}
],
"nextCursor": "opaque-cursor-token"
}// Page through every chart
let cursor: string | undefined;
do {
const { items, nextCursor } = await szum.charts.list({ cursor });
// … handle items …
cursor = nextCursor ?? undefined;
} while (cursor);Listing is read-only and never counts against your render quota. This is the way to enumerate charts created via the API – the Studio gallery shows your first-party charts (Figma and the in-app editor) and counts API-created charts rather than listing them.
Get a chart – GET /api/charts/{id}
Read one chart object by id. Owner-only – a non-owner gets 404, never a hint the id exists.
curl https://szum.io/api/charts/abc123 -H "Authorization: Bearer YOUR_API_KEY"const chart = await szum.charts.get("abc123");Get a chart's config – GET /api/charts/{id}/config
Read a chart's config (the definition you saved). Owner-only. Returns { "config", "draft", "publishedAt", "title" }:
| Field | Notes |
|---|---|
config | The published config, or null for a chart that has never been published (an editor draft with no live version yet). |
draft | The latest unpublished edits as a config, or null when there's no pending draft. Drafts come from the in-app editor; API-created charts never have one. |
publishedAt | ISO-8601 timestamp or null (null = the public URLs are dark). |
title | The chart's title. |
curl https://szum.io/api/charts/abc123/config -H "Authorization: Bearer YOUR_API_KEY"const config = await szum.charts.getConfig("abc123");Get configs in batch – GET /api/charts/configs
Read several charts' configs in one request – the batch form of the above. Pass ?ids= (comma-separated, max 100). Owner-only, partial result degrades gracefully. Match results by id, not by request position – configs isn't ordered to mirror your input. Any requested id that didn't return a config is listed in missing with a reason. Like source, reason is an open set – match the reasons you handle (today "not_found" for unowned or absent, "unavailable" for a transient storage error – safe to retry). Returns { "configs": [{ "id", "config" }], "missing": [{ "id", "reason" }] }.
curl "https://szum.io/api/charts/configs?ids=abc123,def456" -H "Authorization: Bearer YOUR_API_KEY"const configs = await szum.charts.getConfigs(["abc123", "def456"]);Replace a config – PUT /api/charts/{id}/config
Replace a chart's config in place – the id and its /c/ + /e/ URLs stay the same, and the edge caches are purged so the new version serves immediately. Owner-only. Returns the updated chart object.
curl -X PUT https://szum.io/api/charts/abc123/config \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"config":{"version":"2026-03-20","format":"png","marks":[{"type":"barY","data":[{"x":"Q1","y":58}]}]}}'const chart = await szum.charts.update("abc123", {
format: "png",
marks: [{ type: "barY", data: [{ x: "Q1", y: 58 }] }],
});Rename a chart – PATCH /api/charts/{id}
Update a chart's title – metadata only. The config, the /c/ + /e/ URLs, and the cached renders are untouched (no purge needed). Owner-only. Returns the updated chart object.
curl -X PATCH https://szum.io/api/charts/abc123 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Q2 revenue, final"}'Delete a chart – DELETE /api/charts/{id}
Revoke a single saved chart. After deletion, the URL returns 404. You can only delete charts you own; bulk deletion is in Account → Settings.
curl -X DELETE https://szum.io/api/charts/abc123 -H "Authorization: Bearer YOUR_API_KEY"await szum.charts.delete("abc123");Rendering
GET /c/{id}
Public render endpoint. Returns the rendered image. CDN-cached – repeated fetches of the same URL serve from edge cache. See Caching & CDN for exact TTLs.
<img src="https://szum.io/c/abc123" alt="Quarterly revenue" />Each fresh render counts as one render against the creator's monthly limit (the user who saved the chart, not the recipient opening the email).
PNG or SVG from one id
The bare URL renders in the format you saved the chart with. To get a specific format from the same id – without saving the chart twice – add a .png or .svg extension:
<!-- PNG for email (Outlook and friends don't render SVG in mail) -->
<img src="https://szum.io/c/abc123.png" alt="Quarterly revenue" />
<!-- SVG for the web – sharp at any size, smaller payload -->
<img src="https://szum.io/c/abc123.svg" alt="Quarterly revenue" />/c/{id}– the format from the saved config (default)./c/{id}.png– force PNG (retina raster), regardless of the saved format./c/{id}.svg– force SVG.
Each variant is its own cache entry, so the PNG and SVG links cache independently. A good default when saving is PNG – it renders everywhere, including the email clients that drop SVG. Reach for .svg when you control the surface and want crispness at any size. (Interactive embeds at /e/{id} don't take a format – they're live HTML.)
Config size
Per-config limit is 50 KB. Larger configs return 413.
Plan and storage
Saved charts work on both Free and Pro – the difference is storage budget. Free accounts get a small storage allowance (enough for hundreds of typical charts); Pro accounts have an effectively unlimited allowance for normal use. New saves return 413 once your account's storage is full – delete unused charts or upgrade.
Only cache-miss loads of /c/{id} or /e/{id} count – one render against the creator's monthly quota. CDN cache hits are served from the edge for free; there is no separate per-view charge.
On Pro → Free downgrade
Your saved charts stay live on any plan – every /c/ and /e/ URL keeps rendering after you move to Free.
The only difference is the storage budget. If your saved charts exceed the Free allowance, you can't save new charts (or grow existing ones) until you delete some or upgrade again; the charts you've already saved keep working.
Saved charts are removed only when you remove them – per chart, or "Delete all charts" in Account → Settings – or when you delete your account.
Embedding
The URL works anywhere <img src> works:
<img
src="https://szum.io/c/abc123"
alt="Weekly active users, rising to 15,900"
width="540"
height="360"
style="display: block; border: 0; width: 100%; max-width: 540px; height: auto;"
/>Also see Charts in Emails.
Errors
Shares the szum error vocabulary. Saved-chart-specific status codes:
| Status | Meaning |
|---|---|
308 | Request URL had a query string. Permanent redirect to the canonical bare /c/{id}; browsers and image loaders follow automatically. Cached for 1d. |
404 | Chart was deleted or unpublished, never existed, or a JSON endpoint addressed a chart you don't own |
409 | A POST with an Idempotency-Key whose first request is still in flight. Includes Retry-After. |
413 | Config too large |
429 | Per-IP burst rate limit on origin hits (cache misses only). Includes Retry-After. CDN cache hits bypass the limiter entirely. |
503 | Storage temporarily unavailable – retry after Retry-After: 2. |
Rate limits
The JSON API (POST, GET, PUT, DELETE under /api/charts) shares the same per-key burst limit as the render API: 30 requests per second. Image reads (GET /c/{id}) inherit unauthenticated burst limits and count against the creator's monthly render quota on cache miss.
See API → Rate limits for the full picture.
See also
- Interactive embeds – same id as
/e/{id}for iframe-based interactive embeds - API – synchronous
/chartrender endpoint - Authentication – getting an API key
- Plans & Limits – Pro plan details
- Charts in Emails – embedding patterns