chartshot
Docs menu

API

Every endpoint, response headers, usage, errors and limits.

The API lives at https://api.chartshot.dev. A chart is a GET request, so it works anywhere an image URL does.

Authentication

Chart URLs work without a key, with the "chartshot.dev" badge. Everything else takes a key from the Keys page: send the secret key as Authorization: Bearer sk_live_…, or sign a chart URL with it. Keys and signed URLs explains both.

GET /v1/chart

Draws the chart a spec describes and returns the image.

ParameterValue
cThe spec: base64url-encoded JSON, with or without = padding, or plain JSON that starts with {. Required.
errorsimage (the default) sends an error as an image. status sends it as JSON with its real HTTP status.
k, exp, sigA signed URL: your public id, an optional expiry in Unix seconds, and the signature.

Any other parameter is an error, so a typo doesn't go unnoticed. A secret key (sk_…) in the URL is refused, and should be rolled.

Shell
curl -o chart.png "https://api.chartshot.dev/v1/chart?c=eyJ0eXBlIjoiYmFyIiwiZGF0YSI6eyJsYWJlbHMiOlsiTW9uIiwiVHVlIl0sInNlcmllcyI6W3sibmFtZSI6IlNpZ251cHMiLCJ2YWx1ZXMiOlsxMiwxOF19XX19"

The image's format is the spec's format: png (the default), svg or webp. An SVG can't run scripts or load anything when opened on its own.

POST /v1/render

Draws the spec in the body (JSON, up to 64 KB) and returns the image, for code that handles the image itself. It takes a secret key, and its errors are always JSON.

Shell
curl https://api.chartshot.dev/v1/render \
  -H "Authorization: Bearer $CHARTSHOT_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d @spec.json -o chart.png

POST /v1/charts

Draws the spec in the body and stores the image at a permanent URL, for places that keep a link for a long time. Stored charts come with Pro and up, and are kept for 90 days on Pro and a year on Team and Business.

Shell
curl https://api.chartshot.dev/v1/charts \
  -H "Authorization: Bearer $CHARTSHOT_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d @spec.json
JSON
{
  "id": "7fQ2mZk1Lr8xWc3NvB0pYd",
  "url": "https://c.chartshot.dev/7fQ2mZk1Lr8xWc3NvB0pYd.png",
  "format": "png",
  "width": 600,
  "height": 300,
  "alt": "Bar chart. Signups, highest 18 (Tue), lowest 12 (Mon).",
  "expiresAt": "2027-01-22T09:30:00.000Z"
}

The id is random, so the URL can't be guessed. DELETE /v1/charts/{id} deletes a chart, and its URL stops working within a minute. The dashboard lists your stored charts.

GET /v1/usage

This month's renders for your account, every key together:

JSON
{
  "plan": "pro",
  "period": { "start": "2026-10-01", "end": "2026-11-01" },
  "renders": { "used": 1240, "included": 10000, "limit": 11000, "remaining": 8760 },
  "days": [{ "date": "2026-10-01", "renders": 310 }]
}

days lists the days with renders. Counts can lag by up to a minute.

Saved themes

Save a theme once and use it by id in any spec drawn with your key: "theme": "theme_…". A saved theme is the same object as an inline theme (extends, font, light, dark). The dashboard has an editor that imports a shadcn globals.css.

RequestWhat it does
GET /v1/themesLists your themes, oldest first, as { "data": [...] }.
POST /v1/themesSaves { "name": "Brand", "theme": { … } } and returns it with its id.
GET /v1/themes/{id}One theme.
PATCH /v1/themes/{id}Changes its name, theme or both. Charts drawn after that use the new colours.
DELETE /v1/themes/{id}Deletes it. Specs that name it then get theme_not_found.

Pro saves three themes, Team and Business any number. Keyless chart URLs can't use saved themes.

Usage

Renders are counted per calendar month in UTC, for every plan, so the count resets on the 1st. Each chart a key gets counts once, whether it was drawn or came from the API's memory; a 304 and an error don't count, and neither do copies the CDN serves.

Past your plan's renders, charts keep coming for another 10%. After that, POST requests get usage_limit_reached (402) and signed URLs an image saying the limit is reached, until the month ends or you upgrade. The account's email gets a note at 80% and at 100%.

PlanRenders a monthWith the 10% graceA minute per keySaved themesStored charts keptSigned URLs
Free50055060Built-in onlyNoneNo
Pro10,00011,000300390 daysYes
Team50,00055,000300Unlimited1 yearYes
Business250,000275,000300Unlimited1 yearYes

Response headers

HeaderWhat it holds
Content-Typeimage/png, image/svg+xml or image/webp.
Cache-Controlpublic, max-age=3600 for keyless URLs, a year (or until exp) for signed URLs, no-store for POST. Error responses are never cached.
ETagA hash of the spec and the renderer version. Send it back as If-None-Match to get a 304 while the chart is unchanged.
X-Chart-AltAlt text describing the chart, such as Bar chart. Signups, highest 18 (Tue), lowest 12 (Mon). Set the spec's alt to use your own.
X-Chart-WarningsFields the spec doesn't know, which were ignored, each with a hint. Present only when there are some.
X-RateLimit-Limit, X-RateLimit-RemainingRequests allowed a minute (from your IP without a key, per key with one), and how many are left. Retry-After comes with a 429.
X-Usage-Limit, X-Usage-RemainingWith a key: your plan's renders this month, and how many are left.
X-Error-CodeThe error's code, on error images and JSON alike.
X-Request-IdOur id for the request. Quote it if you report a problem.

Warnings

A field the spec doesn't know doesn't stop the chart. It is drawn without that field, and X-Chart-Warnings says what was ignored. Putting title at the top level instead of in options gives:

Text
X-Chart-Warnings: title: Not a field, so it was ignored. Did you mean options.title?

Several warnings are joined with ; . Each one is percent-encoded on its own, ; included, so split on ; and decode each part (decodeURIComponent in JavaScript) to get the text back.

The playground shows the same warnings beside the editor.

Errors

An <img> can't show JSON, and shows any non-200 response as a broken image. So by default an error comes back as an image with status 200: a short message drawn at the size the spec asked for, with the code in X-Error-Code.

Add errors=status while you debug a URL, and the error comes back as JSON with its real status. Requests with a secret key always get JSON. issues lists each field at fault by its path in the spec:

JSON
{
  "error": {
    "code": "invalid_spec",
    "message": "data.series[0].values: Expected 2 values, one per label, got 1",
    "issues": [
      { "path": "data.series[0].values", "message": "Expected 2 values, one per label, got 1" }
    ]
  }
}

Error codes never change their meaning, so code can check them:

CodeStatusMessage
invalid_spec400The chart spec isn't valid. Check the listed fields.
invalid_json400The chart spec isn't valid JSON.
invalid_encoding400The c parameter must be base64url-encoded JSON, or plain JSON starting with {.
spec_too_large413The chart spec is over 64 KB. Send fewer values or shorter labels.
query_too_large414The URL is over 8 KB. POST the spec, or store the chart and use its short URL.
invalid_request400The request isn't valid. Check the listed fields.
theme_not_found400The saved theme wasn't found. Check the theme id.
missing_api_key401Add your secret key as an Authorization: Bearer header.
invalid_api_key401This key isn't valid. It may have been rolled.
invalid_signature401The URL doesn't match its signature. Sign it again, and send the signed text as it is, then &sig=.
signature_expired401This signed URL has expired. Sign a new one.
secret_key_in_url400Secret keys must never go in a URL. Roll this key now, then sign URLs instead.
plan_upgrade_required403Your plan doesn't include this. Upgrade to use it.
usage_limit_reached402This month's renders are used up. Upgrade, or wait for the next period.
rate_limited429Too many requests. Try again in a minute.
forbidden403Requests must go through api.chartshot.dev.
not_found404Nothing was found here.
render_timeout500Drawing this chart took too long. Try again, or send fewer values.
server_busy503Too many charts are being drawn right now. Try again.
internal_error500Something went wrong on our side. Try again.

A series of only null values, a single value and null gaps are not errors: each draws a chart. A null leaves a gap in a line, and a lone value is drawn as a dot.

Limits

Each limit exists for a reason on the server or in an image format. Anything else a spec asks for is drawn.

LimitValueWhy
Query string8 KBSome proxies and clients refuse longer URLs.
Values in one chart5,000Labels × series, slices, rings, points or sparkline values. More would take longer to draw than the render timeout allows.
Width and height1 to 5,000 pxWebP can't be over 16,383 px a side, even at scale 3.
Output pixels16m (5m for WebP, 5m without a key)Width × height × scale², which sets how much memory and time a render takes.
Alt text1,000 charactersIt comes back in the X-Chart-Alt header, and headers have a size limit.
Keyless requests60 a minute per IPImages the CDN already has don't count.
Requests with a keyYour plan's, a minute per keyOne key can't take every render thread. See the plans table.

POST /mcp

Chartshot's MCP server, for AI assistants, at https://api.chartshot.dev/mcp. It takes a secret key as Authorization: Bearer sk_live_…, like the other routes, and charts drawn through it count against your plan.

GET /v1/schema

Returns the spec's JSON Schema (draft 2020-12). Point an editor at it for autocomplete, or give it to a language model. It allows requests from any origin and is cached for an hour.

Shell
curl https://api.chartshot.dev/v1/schema

The spec reference is built from the same schema.