Docs menu
Keys and signed URLs
Render with your secret key, sign chart URLs for emails and Slack, and roll a key.
Sign in and create a key on the Keys page. A key has two parts:
| Part | Looks like | Where it goes |
|---|---|---|
| Secret key | sk_live_ and 32 characters | Your server only: the Authorization header, and signing URLs. The dashboard shows it once, when you create or roll the key. |
| Public id | pk_live_ and 24 characters | The k parameter of signed URLs. It's safe to share. |
Charts made with a key count against your plan's renders, and on paid plans they have no badge.
Render with your key
Send the spec as the body of POST /v1/render, with the secret key as a bearer token:
curl https://api.chartshot.dev/v1/render \
-H "Authorization: Bearer $CHARTSHOT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"bar","data":{"labels":["Mon","Tue"],"series":[{"name":"Signups","values":[12,18]}]}}' \
-o chart.pngUse it where your code handles the image itself: attaching it to an email, uploading it, or saving it. Errors come back as JSON with each field at fault, as in the API reference.
Signed URLs
A signed URL is a chart URL made with your key, for places that load an image by URL, such as an email or a Slack message. It draws without the badge, counts against your plan, and is cached for a year, or until it expires. Anyone with the URL can see the chart, but changing any part of it breaks the signature. Signed URLs come with Pro and up.
In JavaScript or TypeScript, the SDK signs them for you:
import { Charts } from "@lglab/chartshot";
const charts = new Charts({
secretKey: process.env.CHARTSHOT_SECRET_KEY!,
publicId: process.env.CHARTSHOT_PUBLIC_ID,
});
const url = charts.signUrl(spec, { expiresIn: "90d" });In any other language, to sign a URL:
- Make the key's signing key: HMAC-SHA256 of the text
chartshot-sign, keyed with the secret key. - Set the parameters:
c(the spec, base64url-encoded JSON),k(the public id) and, if the URL should stop working at some point,exp(Unix time in seconds).errorscan be signed too. - Percent-encode each name and value (RFC 3986: everything except
A–Z a–z 0–9 - . _ ~), sort them by name, and join them asname=valuepairs with&. - Add
&sig=and the base64url HMAC-SHA256 of that text, keyed with the signing key, without=padding. The URL's query is that text exactly: one signature fits one URL, so a reordered or re-encoded copy is refused.
import { createHmac } from "node:crypto";
const secret = process.env.CHARTSHOT_SECRET_KEY; // sk_live_…
const publicId = process.env.CHARTSHOT_PUBLIC_ID; // pk_live_…
const signingKey = createHmac("sha256", secret).update("chartshot-sign").digest();
/** RFC 3986: everything but A–Z a–z 0–9 - . _ ~ is percent-encoded. */
const encode = (text) =>
encodeURIComponent(text).replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
export function signedChartUrl(spec, { expiresAt } = {}) {
const params = new URLSearchParams({
c: Buffer.from(JSON.stringify(spec)).toString("base64url"),
k: publicId,
});
if (expiresAt) params.set("exp", String(Math.floor(expiresAt.getTime() / 1000)));
const signed = [...params]
.map(([name, value]) => [encode(name), encode(value)])
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
.map(([name, value]) => `${name}=${value}`)
.join("&");
const sig = createHmac("sha256", signingKey).update(signed).digest("base64url");
return `https://api.chartshot.dev/v1/chart?${signed}&sig=${sig}`;
}import base64, hashlib, hmac, json, os, time
from urllib.parse import quote
secret = os.environ["CHARTSHOT_SECRET_KEY"] # sk_live_…
public_id = os.environ["CHARTSHOT_PUBLIC_ID"] # pk_live_…
signing_key = hmac.new(secret.encode(), b"chartshot-sign", hashlib.sha256).digest()
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
def signed_chart_url(spec: dict, expires_at: float | None = None) -> str:
params = {"c": b64url(json.dumps(spec, separators=(",", ":")).encode()), "k": public_id}
if expires_at is not None:
params["exp"] = str(int(expires_at))
signed = "&".join(f"{quote(k, safe='')}={quote(v, safe='')}" for k, v in sorted(params.items()))
sig = b64url(hmac.new(signing_key, signed.encode(), hashlib.sha256).digest())
return f"https://api.chartshot.dev/v1/chart?{signed}&sig={sig}"A signed URL that doesn't check out is refused with one of these codes, as an image or, with errors=status, as JSON:
| Code | Why |
|---|---|
invalid_signature | The URL changed after it was signed, its parameters aren't in the signed order, or it was signed with another key. |
signature_expired | exp has passed. |
invalid_api_key | No live key has this public id: it was rolled or revoked. |
plan_upgrade_required | The key's plan doesn't include signed URLs. |
Roll or revoke a key
Rolling a key gives it a new secret key, public id and signing key. The old secret and every URL signed with it stop working within a minute. Revoking a key ends it for good.
To change keys without a gap, create a second key, move your code to it, then revoke the first.
Keep the secret key out of browsers, URLs and repositories. A URL that contains sk_ is refused with secret_key_in_url; roll that key.