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}
AuthKeylessAPI key to write, keyless to read
URL shapeFull config in queryShort opaque id
Config sizeURL-length limitedUp to 50 KB
Use casePrototyping, no-authProduction 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"
}
FieldNotes
idThe opaque capability. Holds for /c/, /e/, and the JSON endpoints.
sourceWhere 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.
titleAn explicit title in the request body if given, else the config's title (empty string if neither).
createdAtISO-8601 timestamp, e.g. 2024-06-01T00:00:00.000Z.
updatedAtISO-8601 timestamp; equals createdAt until you replace the config in place (PUT) or rename (PATCH), then it's bumped.
sizeBytesStored (compressed) size.
publishedAtISO-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.
imageUrlRendered image (/c/{id}). Add .png / .svg to force a format.
embedUrlInteractive HTML embed (/e/{id}).
configUrlThe 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:

ParamDefaultNotes
sourceallOmit to list every chart, or filter to one or a comma-separated set of figma / api / app / mcp (e.g. ?source=figma,app).
sortcreatedcreated (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.
qCase-insensitive title substring filter. When present, the response also carries total – the exact match count.
limit100Page size, max 1000.
cursorOmit 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" }:

FieldNotes
configThe published config, or null for a chart that has never been published (an editor draft with no live version yet).
draftThe 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.
publishedAtISO-8601 timestamp or null (null = the public URLs are dark).
titleThe 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 positionconfigs 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;"
/>
![Weekly active users](https://szum.io/c/abc123)

Also see Charts in Emails.

Errors

Shares the szum error vocabulary. Saved-chart-specific status codes:

StatusMeaning
308Request URL had a query string. Permanent redirect to the canonical bare /c/{id}; browsers and image loaders follow automatically. Cached for 1d.
404Chart was deleted or unpublished, never existed, or a JSON endpoint addressed a chart you don't own
409A POST with an Idempotency-Key whose first request is still in flight. Includes Retry-After.
413Config too large
429Per-IP burst rate limit on origin hits (cache misses only). Includes Retry-After. CDN cache hits bypass the limiter entirely.
503Storage 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

On this page