# A URL that makes a chart

Open an editable JSURL2 link or send JSON. Get back PNG, SVG, or WebP bytes.

## Open a chart with GET

The render route is `GET https://vizzo.dev/x`. Put the render envelope in the `data` parameter and add output options as query parameters.

```text
https://vizzo.dev/x?width=960&height=540&theme=light&data=(definition~(marks~!(type~lineY~data~!(month~Jan~revenue~42000)(month~Feb~revenue~58000)(month~Mar~revenue~76000)(month~Apr~revenue~64000)(month~May~revenue~81000)(month~Jun~revenue~93000)~options~(x~month~y~revenue~points~~stroke~**H2563eb))~scales~(x~(scale~point~padding~0.2~label~Month)y~(scale~linear~nice~~grid~~label~Revenue_*CUSD*D)))theme~light)~
```

[Open the chart](https://vizzo.dev/x?width=960&height=540&theme=light&data=(definition~(marks~!(type~lineY~data~!(month~Jan~revenue~42000)(month~Feb~revenue~58000)(month~Mar~revenue~76000)(month~Apr~revenue~64000)(month~May~revenue~81000)(month~Jun~revenue~93000)~options~(x~month~y~revenue~points~~stroke~**H2563eb))~scales~(x~(scale~point~padding~0.2~label~Month)y~(scale~linear~nice~~grid~~label~Revenue_*CUSD*D)))theme~light)~), then edit `width`, `height`, or `theme` in the URL. Query options override the same fields inside the envelope. The API returns image bytes, not a page or a JSON wrapper.

## JSURL2 is the default for our examples

JSURL2 encodes the existing JSON envelope with readable separators. It does not change the chart grammar. Use the small `jsurl2` package to create a link.

```ts
import { stringify } from 'jsurl2';

const envelope = {
  definition: {
    marks: [{
      type: 'barY',
      data: [['A', 4], ['B', 8], ['C', 6]],
      options: { x: '0', y: '1' },
    }],
    scales: {
      x: { scale: 'band' },
      y: { scale: 'linear' },
    },
  },
};

const url = `https://vizzo.dev/x?width=800&data=${stringify(envelope)}`;
```

JSURL2 escapes URL delimiters itself. Keep its string directly after `data=` to preserve readable parentheses and tildes. An additional `URLSearchParams` encoding also works, but escapes some of that punctuation again. Do not invent or hand-encode JSURL2 escapes; use the library when values contain reserved characters or Unicode.

## Raw JSON and base64 work too

All three encodings use the same `data` parameter. The API detects the encoding and validates the decoded envelope with the same Zod schema as POST.

```ts
// Raw JSON, safely percent-encoded
const raw = new URL('https://vizzo.dev/x');
raw.searchParams.set('data', JSON.stringify(envelope));

// UTF-8 JSON as base64url in Node or Bun
const data = Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64url');
const base64 = `https://vizzo.dev/x?data=${data}`;
```

Standard base64 also works. Encode it with `URLSearchParams` so `+` does not become a space. Base64 hides the chart structure and is not compression. Minify JSON before encoding raw JSON or base64. JSURL2 already serializes an object without JSON formatting whitespace.

## Send JSON with POST

`POST https://vizzo.dev/` accepts an envelope as an `application/json` body. Use POST when a chart has many rows or a link would exceed a browser, proxy, or chat application's URL limit.

```sh
curl https://vizzo.dev/ \
  -H 'Content-Type: application/json' \
  --data-binary @line.json \
  --output line.png
```

```ts
const response = await fetch('https://vizzo.dev/', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ ...envelope, format: 'webp', theme: 'dark' }),
});

if (!response.ok) throw new Error(await response.text());
const image = await response.arrayBuffer();
```

## Output options

| Field | Default | Accepted values |
| --- | --- | --- |
| `data` | Required for GET | JSURL2, percent-encoded JSON, base64, or base64url render envelope |
| `definition` | Required inside the envelope | TanStack Charts definition |
| `width` | 960 | Integer from 1 to 2,000 |
| `height` | 540 | Integer from 1 to 2,000 |
| `format` | `png` | `png`, `svg`, `webp` |
| `theme` | Chart's theme | `light`, `dark` |
| `preset` | None | `og`, `twitter`, `linkedin`, `discord` |
| `background` | Theme background | PNG/WebP canvas color behind transparent or uncovered chart areas |

Dimensions from `preset` apply when no explicit width or height is given. On GET, query fields override fields in `data`. Use a field once; duplicate or unknown query parameters are rejected. POST dimensions must be JSON numbers, not strings.

`HEAD /x` returns GET headers without an image body and shares its cache entry. On a cache miss it consumes a render request. Other methods on `/x` return `405`. `OPTIONS /` handles browser CORS preflight without consuming a render request. The API permits cross-origin requests.

## Image caching

Successful GET images are publicly cached for 30 days in browsers and Cloudflare's Workers cache. The cache key includes the complete path and query string, so changes to `data`, `theme`, dimensions, or format use separate entries. Keep the query parameter order consistent when sharing a URL.

Cache hits return the image without invoking the renderer or rate limiter. POST responses and errors are not cached. Cloudflare starts a new cache on deployment, and may evict entries before their freshness period ends.

Every chart preview on this site uses the API with an explicit light or dark theme. Previews follow your device's color preference.

## Public limits and errors

Uncached GET renders and POST share an allowance of 10 render requests per minute per IP address. Cloudflare's limiter is approximate and local to each location. Cached GET images do not consume the allowance.

The JSON payload limit is 1 MiB, measured after decoding. Encoded GET inputs are also bounded before parsing. Output dimensions are at most 2,000 × 2,000. The sum of all mark data arrays must not exceed 10,000 rows; two marks over the same 6,000 rows count as 12,000.

| Status | Meaning | What to change |
| --- | --- | --- |
| `200` | Image bytes | Read `Content-Type` to identify the format |
| `400` | Invalid encoding, JSON, options, definition, or render input | Read the JSON error and validation `issues` when present |
| `405` | Unsupported method | Use GET on `/x` or POST on `/` |
| `413` | Request data is too large | Reduce input size or row count; POST still has the 1 MiB body limit |
| `415` | Unsupported POST content type | Set `Content-Type: application/json` |
| `429` | Rate limit exceeded | Wait before requesting another render |
| `500` | A schema-valid definition could not render | Check mark options, field mappings, and scales |

Dimension and row limits fail validation with `400`. Oversized input bytes return `413`. Errors are JSON with an `error` message. Download the image and upload the file to your messaging platform, or reuse its URL. A new URL or a cache miss counts toward the render allowance.
