# API reference

An API key is a password for programs: it lets a script or an AI assistant work with your charts without signing in as you. Create one on your [account page](https://chartjuice.com/account), under API keys. The Chartjuice chart API is a REST API: with a key, a program can make a chart from CSV or JSON, edit it, and get it back as a PNG or SVG.

## Your first chart

You need a key first. On your [account page](https://chartjuice.com/account), under API keys, name a key and choose Create key. Copy it when it appears, because it is shown once.

The examples read the key from an environment variable called `CHARTJUICE_API_KEY`, so it never has to be written into a file. In a terminal on macOS or Linux, set it like this, with your own key:

```bash
export CHARTJUICE_API_KEY="paste-your-key-here"
```

Now make a chart. This sends three months of sales as CSV and asks for a bar chart with a title:

curl:

```bash
curl -X POST "https://chartjuice.com/api/v1/charts" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "bar",
    "data": "Month,Sales\nJan,4200\nFeb,5100\nMar,4800",
    "ops": [{ "op": "setTitle", "title": { "text": "Monthly sales" } }]
  }'
```

JavaScript:

```javascript
import { writeFile } from 'node:fs/promises'

const api = 'https://chartjuice.com/api/v1'
const headers = { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}`, 'Content-Type': 'application/json' }

// 1. Make a bar chart from CSV.
const created = await fetch(`${api}/charts`, {
  method: 'POST',
  headers,
  body: JSON.stringify({ type: 'bar', data: 'Month,Sales\nJan,4200\nFeb,5100\nMar,4800' }),
})
const { chart } = await created.json()

// 2. Edit it with ops: give it a title, and put the largest bar first.
await fetch(`${api}/charts/${chart.id}/ops`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    ops: [
      { op: 'setTitle', title: { text: 'Monthly sales' } },
      { op: 'addTransform', transform: { type: 'sort', column: 'c2', direction: 'desc' } },
    ],
  }),
})

// 3. Download it as an image.
const image = await fetch(`${api}/charts/${chart.id}/render.png?scale=2`, { headers })
await writeFile('chart.png', Buffer.from(await image.arrayBuffer()))

console.log(`Saved chart.png. Open the chart at ${chart.url}`)
```

Python:

```python
import json
import os
import urllib.request

API = "https://chartjuice.com/api/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"], "Content-Type": "application/json"}


def call(method, path, body=None):
    data = json.dumps(body).encode() if body is not None else None
    request = urllib.request.Request(API + path, method=method, headers=HEADERS, data=data)
    with urllib.request.urlopen(request) as response:
        return response.read()


# 1. Make a bar chart from CSV.
created = json.loads(call("POST", "/charts", {"type": "bar", "data": "Month,Sales\nJan,4200\nFeb,5100\nMar,4800"}))
chart = created["chart"]

# 2. Edit it with ops: give it a title, and put the largest bar first.
call("POST", f"/charts/{chart['id']}/ops", {
    "ops": [
        {"op": "setTitle", "title": {"text": "Monthly sales"}},
        {"op": "addTransform", "transform": {"type": "sort", "column": "c2", "direction": "desc"}},
    ],
})

# 3. Download it as an image.
with open("chart.png", "wb") as file:
    file.write(call("GET", f"/charts/{chart['id']}/render.png?scale=2"))

print(f"Saved chart.png. Open the chart at {chart['url']}")
```

The answer is JSON. It has the chart's `id`, and a `url` you can open in a browser to see the chart in the editor. To download the chart as an image, put the id in place of `$CHART_ID`:

```bash
curl "https://chartjuice.com/api/v1/charts/$CHART_ID/render.png?scale=2" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY" \
  -o chart.png
```

That saves `chart.png`, at twice the chart's size. The JavaScript and Python tabs do all of it in one short program, with one more step in the middle: an edit that sorts the bars.

## Authentication and requests

Every address starts with `https://chartjuice.com/api/v1`.

Send your key in the `Authorization` header of every request, as the word `Bearer`, a space, and the key. Anyone who has the key can read, change and delete your charts, so keep it out of web pages and public code. If a key gets out, revoke it on your account page and make a new one.

An AI assistant that you connected by signing in sends an access token in the same header, in place of a key. It got the token when you chose Allow, through OAuth, and it works at every address here within the same limits. A token lasts a short while and the assistant renews it by itself. To end its access, disconnect the app on your account page. [Connect an AI assistant](https://chartjuice.com/docs/mcp/) has the steps, and what a connected app can and can't do.

Requests that send data send it as JSON, with a `Content-Type: application/json` header. Answers are JSON, except the endpoints that draw a chart, which answer with the image itself. [Chart images](https://chartjuice.com/docs/images/) covers those. A field the endpoint does not know is an error, so a misspelled field name is caught and not ignored.

Every chart in an answer has a `url`: the page where a person opens it in the editor. Hand that link to whoever asked for the chart.

Each answer also says how much of your request allowance is left, in three headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (the seconds until the count starts again). [Limits and errors](https://chartjuice.com/docs/limits/) has the numbers.

The whole API is also described in one machine-readable file, in the OpenAPI 3.1 format, at `https://chartjuice.com/api/v1/openapi.json`. Tools that generate API clients can read it.

## When a request is refused

A refused request gets an HTTP status of 400 or above and a JSON body like this:

```json
{
  "error": {
    "code": "not_found",
    "message": "There's no chart with that id in this account.",
    "docs": "https://chartjuice.com/docs/limits/#not_found"
  }
}
```

Act on the `code`, which never changes. The `message` is written for a person and says what to do. `docs` links to the code's entry in [Limits and errors](https://chartjuice.com/docs/limits/), where every code is explained.

Any request can be refused with these codes: [`invalid_request`](https://chartjuice.com/docs/limits/#invalid_request), [`unauthorized`](https://chartjuice.com/docs/limits/#unauthorized), [`rate_limited`](https://chartjuice.com/docs/limits/#rate_limited), [`internal`](https://chartjuice.com/docs/limits/#internal). Each endpoint below lists the others it can give.

## Endpoints

This table lists every endpoint with its method and address. Each name leads to its entry further down, which has the fields, an example request and the answer.

| Endpoint | Method and address |
| --- | --- |
| [List charts](https://chartjuice.com/docs/api/#list_charts) | GET /charts |
| [Get a chart](https://chartjuice.com/docs/api/#get_chart) | GET /charts/{id} |
| [Create a chart](https://chartjuice.com/docs/api/#create_chart) | POST /charts |
| [Edit a chart](https://chartjuice.com/docs/api/#apply_ops) | POST /charts/{id}/ops |
| [Rename a chart](https://chartjuice.com/docs/api/#rename_chart) | PATCH /charts/{id} |
| [Duplicate a chart](https://chartjuice.com/docs/api/#duplicate_chart) | POST /charts/{id}/duplicate |
| [Delete a chart](https://chartjuice.com/docs/api/#delete_chart) | DELETE /charts/{id} |
| [Check a chart](https://chartjuice.com/docs/api/#lint_chart) | GET /charts/{id}/lint |
| [Render a chart](https://chartjuice.com/docs/api/#render_chart) | GET /charts/{id}/render.{format} |
| [Replace the data](https://chartjuice.com/docs/api/#set_data) | PUT /charts/{id}/data |
| [Describe the data](https://chartjuice.com/docs/api/#describe_data) | GET /charts/{id}/data/summary |
| [Query the data](https://chartjuice.com/docs/api/#query_data) | POST /charts/{id}/data/query |
| [Render a chart from data](https://chartjuice.com/docs/api/#render_image) | POST /render |
| [Render a chart with new data](https://chartjuice.com/docs/api/#render_template) | POST /charts/{id}/render |
| [Create an image link](https://chartjuice.com/docs/api/#create_image_url) | POST /image-urls |
| [Get the brand palette](https://chartjuice.com/docs/api/#get_brand_palette) | GET /brand-palette |
| [Set the brand palette](https://chartjuice.com/docs/api/#set_brand_palette) | PUT /brand-palette |
| [Get colors from a website](https://chartjuice.com/docs/api/#extract_brand_palette) | POST /brand-palette/extract |
| [Get the account](https://chartjuice.com/docs/api/#get_account) | GET /account |
| [List chart types](https://chartjuice.com/docs/api/#list_chart_types) | GET /chart-types |
| [List examples](https://chartjuice.com/docs/api/#list_examples) | GET /examples |
| [List palettes](https://chartjuice.com/docs/api/#list_palettes) | GET /palettes |
| [Get the op catalog](https://chartjuice.com/docs/api/#get_op_catalog) | GET /ops |
| [Look up the docs](https://chartjuice.com/docs/api/#lookup_docs) | GET /docs/search |

An AI assistant uses these same operations as its tools, so the descriptions are written for an assistant as well as for you. Where one says "the person", it means whoever asked for the chart.

### Charts

#### List charts

GET /charts

The account's saved charts, most recently changed first. Returns each chart's id, name, type and the address a person opens it at. Use it to find a chart someone names, then read it with [`get_chart`](https://chartjuice.com/docs/api/#get_chart). Page through with `cursor`.

| Field | Type | What it is |
| --- | --- | --- |
| `limit` in the query, optional | integer, 1 to 100 default 50 | How many charts to return. |
| `cursor` in the query, optional | string | `next_cursor` from the previous page. |

curl:

```bash
curl "https://chartjuice.com/api/v1/charts?limit=2" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY"
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/charts?limit=2', {
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/charts?limit=2",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "charts": [
    {
      "id": "7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
      "name": "Monthly sales",
      "type": "bar",
      "url": "https://chartjuice.com/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
      "created_at": "2026-10-10T14:03:12.481923+00:00",
      "updated_at": "2026-10-10T14:05:40.118264+00:00"
    },
    {
      "id": "f3a1d2c4-0b5e-4f6a-8c7d-9e0f1a2b3c4d",
      "name": "Visitors by channel",
      "type": "line",
      "url": "https://chartjuice.com/charts/f3a1d2c4-0b5e-4f6a-8c7d-9e0f1a2b3c4d",
      "created_at": "2026-10-10T14:03:12.481923+00:00",
      "updated_at": "2026-10-09T09:12:44.530112+00:00"
    }
  ],
  "next_cursor": "MjAyNi0xMC0wOVQwOToxMjo0NC41MzAxMTIrMDA6MDB8ZjNhMWQyYzQtMGI1ZS00ZjZhLThjN2QtOWUwZjFhMmIzYzRk"
}
```

#### Get a chart

GET /charts/{id}

A chart as it is now, without its rows: its type and options, which column plays which role, the columns, the steps applied to the data, the main style settings and any warnings. Read this before editing a chart you did not make in this conversation, to get the column ids that ops use and to see what is already set. To see how the chart looks, call [`render_chart`](https://chartjuice.com/docs/api/#render_chart).

| Field | Type | What it is |
| --- | --- | --- |
| `id` in the path, required | string | The chart's id. |
| `include` in the query, optional | "spec" | `spec` adds the whole chart spec, data included. |

curl:

```bash
curl "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY"
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21', {
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "chart": {
    "id": "7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    "name": "Monthly sales",
    "type": "bar",
    "url": "https://chartjuice.com/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    "created_at": "2026-10-10T14:03:12.481923+00:00",
    "updated_at": "2026-10-10T14:05:40.118264+00:00"
  },
  "state": {
    "type": "bar",
    "options": {
      "orientation": "vertical",
      "stack": "none"
    },
    "mapping": {
      "category": "c1",
      "values": [
        "c2"
      ]
    },
    "roles": [
      "category: Category",
      "values: Values, numbers"
    ],
    "columns": [
      {
        "id": "c1",
        "name": "Month",
        "kind": "text"
      },
      {
        "id": "c2",
        "name": "Sales",
        "kind": "number"
      }
    ],
    "columnCount": 2,
    "rows": 3,
    "steps": [],
    "columnsAfterSteps": [
      {
        "id": "c1",
        "name": "Month",
        "kind": "text"
      },
      {
        "id": "c2",
        "name": "Sales",
        "kind": "number"
      }
    ],
    "rowsAfterSteps": 3,
    "style": {
      "title": {
        "show": true,
        "text": "Monthly sales"
      },
      "subtitle": {
        "show": false,
        "text": ""
      },
      "legend": {
        "show": "auto",
        "position": {
          "x": "center",
          "y": "bottom"
        }
      },
      "labels": {
        "show": false
      },
      "xAxisTitle": null,
      "yAxisTitle": null,
      "palette": [
        "#0C008C",
        "#2D1FF5",
        "#5D55FA",
        "#8888FC",
        "#C4C6FF"
      ],
      "seriesColors": {},
      "size": "800×500"
    },
    "warnings": []
  }
}
```

Can also be refused with [not_found](https://chartjuice.com/docs/limits/#not_found).

#### Create a chart

POST /charts

Save a new chart. Start from a chart type and your own data (`type` with `data`), from a chart type with sample data (`type` alone), or from a gallery example (`example`). `ops` are applied before it is saved, so one call can make and style a chart. Columns get the ids c1, c2, c3 and so on, in the order they appear in the data, so `ops` can name them in the same call. The answer has the chart's id to use from then on, its `url` to give the person, its columns with their ids, and its warnings, which are what [`lint_chart`](https://chartjuice.com/docs/api/#lint_chart) would report. Call [`render_chart`](https://chartjuice.com/docs/api/#render_chart) next to look at the result, and [`apply_ops`](https://chartjuice.com/docs/api/#apply_ops) to change it. If you are not sure which type suits the data, [`list_chart_types`](https://chartjuice.com/docs/api/#list_chart_types) says what each is for. On the Free plan this is refused with [`chart_limit_reached`](https://chartjuice.com/docs/limits/#chart_limit_reached) once the account has its limit of saved charts.

| Field | Type | What it is |
| --- | --- | --- |
| `type` in the body, optional | "bar" \| "line" \| "pie" \| "scatter" \| "histogram" \| "boxplot" \| "dotplot" \| "sankey" \| "radar" \| "heatmap" | The chart type. On its own, the chart starts as that type with sample data; with `data`, it charts your data. |
| `example` in the body, optional | string | The id of a gallery example to start from ([`list_examples`](https://chartjuice.com/docs/api/#list_examples)), instead of `type`. |
| `data` in the body, optional | string or array | The data. CSV or TSV as one string with a header row, or JSON rows: an array of objects (`[{"Month":"Jan","Sales":4200}]`) or an array of arrays whose first row is the header (`[["Month","Sales"],["Jan",4200]]`). |
| `format` in the body, optional | "csv" \| "tsv" \| "json" | How to read `data`. Left out, a string is read as CSV or TSV (the delimiter is detected) and an array as JSON rows. `json` also takes a string holding JSON. |
| `name` in the body, optional | string | The chart's name in My charts. Left out, the chart's title is used. |
| `ops` in the body, optional | array of objects | Ops to apply, in order. All apply or none do. See the op catalog ([`get_op_catalog`](https://chartjuice.com/docs/api/#get_op_catalog)). |

curl:

```bash
curl -X POST "https://chartjuice.com/api/v1/charts" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "type": "bar",
  "data": "Month,Sales\nJan,4200\nFeb,5100\nMar,4800",
  "ops": [
    {
      "op": "setTitle",
      "title": {
        "text": "Monthly sales"
      }
    }
  ]
}'
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/charts', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "type": "bar",
    "data": "Month,Sales\nJan,4200\nFeb,5100\nMar,4800",
    "ops": [
      {
        "op": "setTitle",
        "title": {
          "text": "Monthly sales"
        }
      }
    ]
  }),
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/charts",
    method="POST",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"], "Content-Type": "application/json"},
    data=json.dumps({
        "type": "bar",
        "data": "Month,Sales\nJan,4200\nFeb,5100\nMar,4800",
        "ops": [{"op": "setTitle", "title": {"text": "Monthly sales"}}],
    }).encode(),
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "chart": {
    "id": "7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    "name": "Monthly sales",
    "type": "bar",
    "url": "https://chartjuice.com/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    "created_at": "2026-10-10T14:03:12.481923+00:00",
    "updated_at": "2026-10-10T14:03:12.481923+00:00"
  },
  "columns": [
    {
      "id": "c1",
      "name": "Month",
      "kind": "text"
    },
    {
      "id": "c2",
      "name": "Sales",
      "kind": "number"
    }
  ],
  "warnings": [],
  "notices": []
}
```

Can also be refused with [invalid_ops](https://chartjuice.com/docs/limits/#invalid_ops), [chart_limit_reached](https://chartjuice.com/docs/limits/#chart_limit_reached) or [too_large](https://chartjuice.com/docs/limits/#too_large).

#### Edit a chart

POST /charts/{id}/ops

Change a chart with ops, the same edits the editor makes: its type, which columns it charts, steps such as sort and top N, titles, axes, colors, and the data itself. [`get_op_catalog`](https://chartjuice.com/docs/api/#get_op_catalog) lists every op and its fields: read it once before the first edit. The ops apply together or not at all; if one is wrong the answer is [`invalid_ops`](https://chartjuice.com/docs/limits/#invalid_ops) with what to fix, and the chart is unchanged. Returns the chart's warnings after the change, which are what [`lint_chart`](https://chartjuice.com/docs/api/#lint_chart) would report, so there is no need to call that as well. Use column ids (c1, c2), not column names: [`create_chart`](https://chartjuice.com/docs/api/#create_chart) answers with them, and [`get_chart`](https://chartjuice.com/docs/api/#get_chart) has them for any chart. Afterwards call [`render_chart`](https://chartjuice.com/docs/api/#render_chart) to check how the chart looks.

| Field | Type | What it is |
| --- | --- | --- |
| `id` in the path, required | string | The chart's id. |
| `ops` in the body, required | array of objects | Ops to apply, in order. All apply or none do. See the op catalog ([`get_op_catalog`](https://chartjuice.com/docs/api/#get_op_catalog)). |
| `expected_updated_at` in the body, optional | string | The chart's `updated_at` as you last saw it. If the chart has changed since, nothing is applied and the answer is [`conflict`](https://chartjuice.com/docs/limits/#conflict). |

curl:

```bash
curl -X POST "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/ops" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "ops": [
    {
      "op": "setVariants",
      "variants": {
        "bar": {
          "orientation": "horizontal"
        }
      }
    },
    {
      "op": "addTransform",
      "transform": {
        "type": "sort",
        "column": "c2",
        "direction": "desc"
      }
    }
  ]
}'
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/ops', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "ops": [
      {
        "op": "setVariants",
        "variants": {
          "bar": {
            "orientation": "horizontal"
          }
        }
      },
      {
        "op": "addTransform",
        "transform": {
          "type": "sort",
          "column": "c2",
          "direction": "desc"
        }
      }
    ]
  }),
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/ops",
    method="POST",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"], "Content-Type": "application/json"},
    data=json.dumps({
        "ops": [
            {"op": "setVariants", "variants": {"bar": {"orientation": "horizontal"}}},
            {
                "op": "addTransform",
                "transform": {"type": "sort", "column": "c2", "direction": "desc"},
            },
        ],
    }).encode(),
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "chart": {
    "id": "7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    "name": "Monthly sales",
    "type": "bar",
    "url": "https://chartjuice.com/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    "created_at": "2026-10-10T14:03:12.481923+00:00",
    "updated_at": "2026-10-10T14:05:40.118264+00:00"
  },
  "applied": 2,
  "warnings": []
}
```

Can also be refused with [not_found](https://chartjuice.com/docs/limits/#not_found), [invalid_ops](https://chartjuice.com/docs/limits/#invalid_ops), [conflict](https://chartjuice.com/docs/limits/#conflict) or [too_large](https://chartjuice.com/docs/limits/#too_large).

#### Rename a chart

PATCH /charts/{id}

Change a chart's name in My charts. The title drawn on the chart is separate: change that with the `setTitle` op.

| Field | Type | What it is |
| --- | --- | --- |
| `id` in the path, required | string | The chart's id. |
| `name` in the body, required | string | The new name. |

curl:

```bash
curl -X PATCH "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Q1 sales"
}'
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21', {
  method: 'PATCH',
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "name": "Q1 sales"
  }),
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    method="PATCH",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"], "Content-Type": "application/json"},
    data=json.dumps({"name": "Q1 sales"}).encode(),
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "chart": {
    "id": "7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    "name": "Q1 sales",
    "type": "bar",
    "url": "https://chartjuice.com/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    "created_at": "2026-10-10T14:03:12.481923+00:00",
    "updated_at": "2026-10-10T14:05:40.118264+00:00"
  }
}
```

Can also be refused with [not_found](https://chartjuice.com/docs/limits/#not_found).

#### Duplicate a chart

POST /charts/{id}/duplicate

Save a copy of a chart, named "Copy of" and the original's name. Use it to try another design without changing the original. The copy counts toward the Free plan's saved charts like any other chart.

| Field | Type | What it is |
| --- | --- | --- |
| `id` in the path, required | string | The chart's id. |

curl:

```bash
curl -X POST "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/duplicate" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY"
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/duplicate', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/duplicate",
    method="POST",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "chart": {
    "id": "0d9e8f7a-6b5c-4d3e-9f2a-1b0c9d8e7f6a",
    "name": "Copy of Monthly sales",
    "type": "bar",
    "url": "https://chartjuice.com/charts/0d9e8f7a-6b5c-4d3e-9f2a-1b0c9d8e7f6a",
    "created_at": "2026-10-10T14:09:02.640771+00:00",
    "updated_at": "2026-10-10T14:09:02.640771+00:00"
  }
}
```

Can also be refused with [not_found](https://chartjuice.com/docs/limits/#not_found) or [chart_limit_reached](https://chartjuice.com/docs/limits/#chart_limit_reached).

#### Delete a chart

DELETE /charts/{id}

Delete a chart for good. There is no undo, so use it only when the person asked for that chart to be deleted.

| Field | Type | What it is |
| --- | --- | --- |
| `id` in the path, required | string | The chart's id. |

curl:

```bash
curl -X DELETE "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY"
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    method="DELETE",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "deleted": true,
  "id": "7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21"
}
```

Can also be refused with [not_found](https://chartjuice.com/docs/limits/#not_found).

#### Check a chart

GET /charts/{id}/lint

Readability problems with a chart as it is now: too many slices, colors too faint to see, an axis that misleads. An empty list means none were found. [`create_chart`](https://chartjuice.com/docs/api/#create_chart), [`apply_ops`](https://chartjuice.com/docs/api/#apply_ops) and [`set_data`](https://chartjuice.com/docs/api/#set_data) already answer with the same list as `warnings`, so call this for a chart you have not just changed. Fix a `warn` with [`apply_ops`](https://chartjuice.com/docs/api/#apply_ops). An `info` is a suggestion: leave it when it is about something the person asked for, such as a color they chose. A pie reads best with 6 slices or fewer: a `topN` step with `n` set to 5 keeps the five largest and adds the rest up as a sixth.

| Field | Type | What it is |
| --- | --- | --- |
| `id` in the path, required | string | The chart's id. |

curl:

```bash
curl "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/lint" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY"
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/lint', {
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/lint",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "problems": [
    {
      "id": "pie-too-many-slices",
      "level": "warn",
      "message": "9 slices is hard to read; group small ones into \"Other\" or use a bar chart"
    }
  ]
}
```

Can also be refused with [not_found](https://chartjuice.com/docs/limits/#not_found).

#### Render a chart

GET /charts/{id}/render.{format}

A saved chart as an image, as it is now, to look at or to hand on: PNG at 1, 2 or 4 times its size, or SVG. Look at the image after an edit to check the chart reads well. The warnings that [`create_chart`](https://chartjuice.com/docs/api/#create_chart) and [`apply_ops`](https://chartjuice.com/docs/api/#apply_ops) answer with cover the problems that are easy to miss by eye; for a chart you have not just changed, [`lint_chart`](https://chartjuice.com/docs/api/#lint_chart) gives them. Counts toward the daily renders. To draw the chart with other data, use [`render_template`](https://chartjuice.com/docs/api/#render_template). On the Free plan the image carries a small chartjuice.com mark, as downloads from the editor do.

| Field | Type | What it is |
| --- | --- | --- |
| `id` in the path, required | string | The chart's id. |
| `format` in the path, required | "png" \| "svg" default "png" | The image format. |
| `scale` in the query, optional | 1 \| 2 \| 4 default 1 | PNG only: 1, 2 or 4 times the canvas size. The image can be up to 4,000 px on a side. |
| `transparent` in the query, optional | boolean default false | Leave out the background. |

curl:

```bash
curl "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/render.png?scale=2" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY" \
  -o chart.png
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/render.png?scale=2', {
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const image = Buffer.from(await response.arrayBuffer())
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/render.png?scale=2",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    image = response.read()
```

Answer

The image itself: a PNG file, or an SVG file from `render.svg`.

Can also be refused with [not_found](https://chartjuice.com/docs/limits/#not_found), [render_limit_reached](https://chartjuice.com/docs/limits/#render_limit_reached) or [image_limit_reached](https://chartjuice.com/docs/limits/#image_limit_reached).

### Data

#### Replace the data

PUT /charts/{id}/data

Replace a chart's data with CSV, TSV or JSON rows, read the way a pasted or uploaded file is (header row, numbers and dates detected). Use it to show new numbers in a chart that is already designed. If the new data has the columns the chart had, by name, the chart keeps its roles, steps and colors. If the columns are different, roles are picked again: read the chart with [`get_chart`](https://chartjuice.com/docs/api/#get_chart) to see what it charts now.

| Field | Type | What it is |
| --- | --- | --- |
| `id` in the path, required | string | The chart's id. |
| `data` in the body, required | string or array | The data. CSV or TSV as one string with a header row, or JSON rows: an array of objects (`[{"Month":"Jan","Sales":4200}]`) or an array of arrays whose first row is the header (`[["Month","Sales"],["Jan",4200]]`). |
| `format` in the body, optional | "csv" \| "tsv" \| "json" | How to read `data`. Left out, a string is read as CSV or TSV (the delimiter is detected) and an array as JSON rows. `json` also takes a string holding JSON. |

curl:

```bash
curl -X PUT "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/data" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "data": [
    {
      "Month": "Apr",
      "Sales": 6300
    },
    {
      "Month": "May",
      "Sales": 7100
    }
  ]
}'
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/data', {
  method: 'PUT',
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "data": [
      {
        "Month": "Apr",
        "Sales": 6300
      },
      {
        "Month": "May",
        "Sales": 7100
      }
    ]
  }),
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/data",
    method="PUT",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"], "Content-Type": "application/json"},
    data=json.dumps({"data": [{"Month": "Apr", "Sales": 6300}, {"Month": "May", "Sales": 7100}]}).encode(),
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "chart": {
    "id": "7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    "name": "Monthly sales",
    "type": "bar",
    "url": "https://chartjuice.com/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    "created_at": "2026-10-10T14:03:12.481923+00:00",
    "updated_at": "2026-10-10T14:05:40.118264+00:00"
  },
  "columns": [
    {
      "id": "c1",
      "name": "Month",
      "kind": "text"
    },
    {
      "id": "c2",
      "name": "Sales",
      "kind": "number"
    }
  ],
  "rows": 2,
  "mapping_kept": true,
  "notices": [],
  "warnings": []
}
```

Can also be refused with [not_found](https://chartjuice.com/docs/limits/#not_found), [conflict](https://chartjuice.com/docs/limits/#conflict) or [too_large](https://chartjuice.com/docs/limits/#too_large).

#### Describe the data

GET /charts/{id}/data/summary

A summary of each column the chart is drawn from, after its steps: numbers get their lowest, highest and average; text and dates get their most common values. Use it to understand data you haven't seen. For the rows themselves, use [`query_data`](https://chartjuice.com/docs/api/#query_data).

| Field | Type | What it is |
| --- | --- | --- |
| `id` in the path, required | string | The chart's id. |

curl:

```bash
curl "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/data/summary" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY"
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/data/summary', {
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/data/summary",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "rows": 3,
  "columns": [
    {
      "id": "c1",
      "name": "Month",
      "kind": "text",
      "blanks": 0,
      "distinct": 3,
      "top": [
        {
          "value": "Jan",
          "count": 1
        },
        {
          "value": "Feb",
          "count": 1
        },
        {
          "value": "Mar",
          "count": 1
        }
      ]
    },
    {
      "id": "c2",
      "name": "Sales",
      "kind": "number",
      "blanks": 0,
      "min": 4200,
      "max": 5100,
      "mean": 4700
    }
  ]
}
```

Can also be refused with [not_found](https://chartjuice.com/docs/limits/#not_found).

#### Query the data

POST /charts/{id}/data/query

Rows of the chart's data after its steps, with extra steps of your own if you like (they are tried, not saved). Use it to check what a sort, filter or total would give before adding it with [`apply_ops`](https://chartjuice.com/docs/api/#apply_ops).

| Field | Type | What it is |
| --- | --- | --- |
| `id` in the path, required | string | The chart's id. |
| `steps` in the body, optional | array of objects | Extra steps to try, in the shape `addTransform` takes. They are not saved. |
| `limit` in the body, optional | integer, 1 to 1,000 default 100 | How many rows to return. |

curl:

```bash
curl -X POST "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/data/query" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "steps": [
    {
      "type": "sort",
      "column": "c2",
      "direction": "desc"
    }
  ],
  "limit": 2
}'
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/data/query', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "steps": [
      {
        "type": "sort",
        "column": "c2",
        "direction": "desc"
      }
    ],
    "limit": 2
  }),
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/data/query",
    method="POST",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"], "Content-Type": "application/json"},
    data=json.dumps({"steps": [{"type": "sort", "column": "c2", "direction": "desc"}], "limit": 2}).encode(),
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "columns": [
    "c1 (Month)",
    "c2 (Sales)"
  ],
  "rows": [
    [
      "Feb",
      5100
    ],
    [
      "Mar",
      4800
    ]
  ],
  "total_rows": 3,
  "errors": []
}
```

Can also be refused with [not_found](https://chartjuice.com/docs/limits/#not_found).

### Images

#### Render a chart from data

POST /render

An image of a chart you describe in the request, with nothing saved: a chart type and your data, or a whole chart spec. Use it for a picture you need once, in a report or a message, where there is no chart to keep. `ops` style it before it is drawn, as they do in [`create_chart`](https://chartjuice.com/docs/api/#create_chart). The answer is the image itself; send `Accept: application/json` to get it base64-encoded in JSON instead. Counts toward the month's image renders: 100 on the Free plan and 1,000 on Pro ([`image_limit_reached`](https://chartjuice.com/docs/limits/#image_limit_reached) after that). Each different picture counts once a month, so asking for the identical image again costs nothing more. On the Free plan the image carries a small chartjuice.com mark.

| Field | Type | What it is |
| --- | --- | --- |
| `type` in the body, optional | "bar" \| "line" \| "pie" \| "scatter" \| "histogram" \| "boxplot" \| "dotplot" \| "sankey" \| "radar" \| "heatmap" | The chart type, with `data`. |
| `data` in the body, optional | string or array | The data. CSV or TSV as one string with a header row, or JSON rows: an array of objects (`[{"Month":"Jan","Sales":4200}]`) or an array of arrays whose first row is the header (`[["Month","Sales"],["Jan",4200]]`). |
| `data_format` in the body, optional | "csv" \| "tsv" \| "json" | How to read `data`. Left out, a string is read as CSV or TSV (the delimiter is detected) and an array as JSON rows. `json` also takes a string holding JSON. |
| `spec` in the body, optional | object | A whole chart spec, in place of `type` and `data`: what [`get_chart`](https://chartjuice.com/docs/api/#get_chart) returns with `include=spec`. Its `advanced` must be `null`. |
| `ops` in the body, optional | array of objects | Ops to apply, in order. All apply or none do. See the op catalog ([`get_op_catalog`](https://chartjuice.com/docs/api/#get_op_catalog)). |
| `name` in the body, optional | string | A name for the image file, used when the image is downloaded. Left out, it is `chart`. |
| `format` in the body, optional | "png" \| "svg" default "png" | The image format. |
| `scale` in the body, optional | 1 \| 2 \| 4 default 1 | PNG only: 1, 2 or 4 times the canvas size. The image can be up to 4,000 px on a side. |
| `width` in the body, optional | integer, 100 to 4,000 | The canvas width in pixels, in place of the chart's own. The image is this times `scale`. |
| `height` in the body, optional | integer, 100 to 4,000 | The canvas height in pixels, in place of the chart's own. The image is this times `scale`. |
| `transparent` in the body, optional | boolean default false | Leave out the background. |

curl:

```bash
curl -X POST "https://chartjuice.com/api/v1/render" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "type": "bar",
  "data": "Month,Sales\nJan,4200\nFeb,5100\nMar,4800",
  "ops": [
    {
      "op": "setTitle",
      "title": {
        "text": "Monthly sales"
      }
    }
  ],
  "scale": 2
}' \
  -o chart.png
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/render', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "type": "bar",
    "data": "Month,Sales\nJan,4200\nFeb,5100\nMar,4800",
    "ops": [
      {
        "op": "setTitle",
        "title": {
          "text": "Monthly sales"
        }
      }
    ],
    "scale": 2
  }),
})
const image = Buffer.from(await response.arrayBuffer())
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/render",
    method="POST",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"], "Content-Type": "application/json"},
    data=json.dumps({
        "type": "bar",
        "data": "Month,Sales\nJan,4200\nFeb,5100\nMar,4800",
        "ops": [{"op": "setTitle", "title": {"text": "Monthly sales"}}],
        "scale": 2,
    }).encode(),
)
with urllib.request.urlopen(request) as response:
    image = response.read()
```

Answer

The image itself: a PNG file, or an SVG file when `format` is `svg`. With the header `Accept: application/json`, the answer is JSON with the image base64-encoded, its type and its size.

Can also be refused with [invalid_ops](https://chartjuice.com/docs/limits/#invalid_ops), [image_limit_reached](https://chartjuice.com/docs/limits/#image_limit_reached) or [too_large](https://chartjuice.com/docs/limits/#too_large).

#### Render a chart with new data

POST /charts/{id}/render

A saved chart drawn with data you send, in place of its own. Design the chart once in the editor, then send new numbers whenever you need the picture: it keeps its type, colors, titles and steps. The saved chart is not changed. The new data's columns are matched to the chart's by name, so it needs every column the chart is drawn from, under the same names; when one is missing the answer is [`invalid_request`](https://chartjuice.com/docs/limits/#invalid_request) and names them. `ops` are applied for this image only. The answer is the image itself; send `Accept: application/json` to get it base64-encoded in JSON instead. Counts toward the month's image renders, not the daily renders. Each different picture counts once a month: the same chart with the same data again costs nothing more.

| Field | Type | What it is |
| --- | --- | --- |
| `id` in the path, required | string | The chart's id. |
| `data` in the body, required | string or array | The data to draw the chart with, in place of its own. Its columns are matched to the chart's by name. CSV or TSV as one string with a header row, or JSON rows: an array of objects (`[{"Month":"Jan","Sales":4200}]`) or an array of arrays whose first row is the header. |
| `data_format` in the body, optional | "csv" \| "tsv" \| "json" | How to read `data`. Left out, a string is read as CSV or TSV (the delimiter is detected) and an array as JSON rows. `json` also takes a string holding JSON. |
| `ops` in the body, optional | array of objects | Ops to apply after the data is in, for this image only. All apply or none do. See the op catalog ([`get_op_catalog`](https://chartjuice.com/docs/api/#get_op_catalog)). |
| `name` in the body, optional | string | A name for the image file, used when the image is downloaded. Left out, it is `chart`. |
| `format` in the body, optional | "png" \| "svg" default "png" | The image format. |
| `scale` in the body, optional | 1 \| 2 \| 4 default 1 | PNG only: 1, 2 or 4 times the canvas size. The image can be up to 4,000 px on a side. |
| `width` in the body, optional | integer, 100 to 4,000 | The canvas width in pixels, in place of the chart's own. The image is this times `scale`. |
| `height` in the body, optional | integer, 100 to 4,000 | The canvas height in pixels, in place of the chart's own. The image is this times `scale`. |
| `transparent` in the body, optional | boolean default false | Leave out the background. |

curl:

```bash
curl -X POST "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/render" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "data": [
    {
      "Month": "Apr",
      "Sales": 6300
    },
    {
      "Month": "May",
      "Sales": 7100
    }
  ],
  "scale": 2
}' \
  -o chart.png
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/render', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "data": [
      {
        "Month": "Apr",
        "Sales": 6300
      },
      {
        "Month": "May",
        "Sales": 7100
      }
    ],
    "scale": 2
  }),
})
const image = Buffer.from(await response.arrayBuffer())
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21/render",
    method="POST",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"], "Content-Type": "application/json"},
    data=json.dumps({"data": [{"Month": "Apr", "Sales": 6300}, {"Month": "May", "Sales": 7100}], "scale": 2}).encode(),
)
with urllib.request.urlopen(request) as response:
    image = response.read()
```

Answer

The image itself: a PNG file, or an SVG file when `format` is `svg`. With the header `Accept: application/json`, the answer is JSON with the image base64-encoded, its type and its size.

Can also be refused with [not_found](https://chartjuice.com/docs/limits/#not_found), [invalid_ops](https://chartjuice.com/docs/limits/#invalid_ops) or [image_limit_reached](https://chartjuice.com/docs/limits/#image_limit_reached).

#### Create an image link

POST /image-urls

A link that shows a saved chart as an image to anyone who has it, with no key and no sign-in: for an `<img>` tag in an email, a doc, a dashboard or another site's page. The link is a live image. It shows the chart as it is now and changes when the chart is edited, so the picture in the email stays current. With `data`, the link draws the chart with that data in place of its own. The link is signed, so nobody can change it to show another chart. It works until its signing key is rotated or revoked on the account page, or for `expires_in` seconds if you give that. The link is for the site it was made on. Making a link costs nothing. Its picture counts toward the month's image renders once each time the chart changes, and once at the start of each month, however many people open it. On the Free plan the image carries a small chartjuice.com mark.

| Field | Type | What it is |
| --- | --- | --- |
| `id` in the body, required | string | The chart's id. |
| `format` in the body, optional | "png" \| "svg" default "png" | The image format. |
| `scale` in the body, optional | 1 \| 2 \| 4 default 1 | PNG only: 1, 2 or 4 times the canvas size. The image can be up to 4,000 px on a side. |
| `width` in the body, optional | integer, 100 to 4,000 | The canvas width in pixels, in place of the chart's own. The image is this times `scale`. |
| `height` in the body, optional | integer, 100 to 4,000 | The canvas height in pixels, in place of the chart's own. The image is this times `scale`. |
| `transparent` in the body, optional | boolean default false | Leave out the background. |
| `data` in the body, optional | string or array | Data to draw the chart with in place of its own. It travels in the link, so it has to be small: up to 4,000 characters once encoded, about 3,000 characters of CSV. Its columns are matched to the chart's by name. Left out, the link is a live image of the chart: it shows the chart as it is now and changes when the chart is edited. |
| `data_format` in the body, optional | "csv" \| "tsv" \| "json" | How to read `data`. Left out, a string is read as CSV or TSV (the delimiter is detected) and an array as JSON rows. `json` also takes a string holding JSON. |
| `expires_in` in the body, optional | integer, 60 to 315,360,000 | Seconds until the link stops working, from one minute to ten years. Left out, it works until its signing key is rotated or revoked. |

curl:

```bash
curl -X POST "https://chartjuice.com/api/v1/image-urls" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "id": "7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
  "scale": 2
}'
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/image-urls', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "id": "7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    "scale": 2
  }),
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/image-urls",
    method="POST",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"], "Content-Type": "application/json"},
    data=json.dumps({"id": "7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21", "scale": 2}).encode(),
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "url": "https://chartjuice.com/img/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21.png?kid=3d2a1b0c-9e8f-4a7b-8c6d-5e4f3a2b1c0d&scale=2&sig=d2bd5b97a1f538c7cb8c860665879d38b7d5a75248202d534d21c3ed3cf0931f",
  "expires_at": null,
  "chart": {
    "id": "7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    "name": "Monthly sales",
    "type": "bar",
    "url": "https://chartjuice.com/charts/7b0c9f1e-6c1d-4c57-9a47-2f1f0a3b8e21",
    "created_at": "2026-10-10T14:03:12.481923+00:00",
    "updated_at": "2026-10-10T14:05:40.118264+00:00"
  }
}
```

Can also be refused with [not_found](https://chartjuice.com/docs/limits/#not_found).

### Brand palette

#### Get the brand palette

GET /brand-palette

The account's saved brand palette: the colors it uses for its own charts. `null` when none is saved. Apply it to a chart with the `setPalette` op.

curl:

```bash
curl "https://chartjuice.com/api/v1/brand-palette" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY"
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/brand-palette', {
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/brand-palette",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "colors": [
    "#0F766E",
    "#F59E0B",
    "#E11D48"
  ]
}
```

#### Set the brand palette

PUT /brand-palette

Save the account's brand palette, replacing the one it had, or clear it with `null`. Saving a palette needs the Pro plan ([`forbidden_plan`](https://chartjuice.com/docs/limits/#forbidden_plan) otherwise).

| Field | Type | What it is |
| --- | --- | --- |
| `colors` in the body, required | array of strings or null | 1 to 12 colors as `#RRGGBB` or `#RGB`, or `null` to clear the palette. |

curl:

```bash
curl -X PUT "https://chartjuice.com/api/v1/brand-palette" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "colors": [
    "#0f766e",
    "#F59E0B",
    "#e11d48"
  ]
}'
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/brand-palette', {
  method: 'PUT',
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "colors": [
      "#0f766e",
      "#F59E0B",
      "#e11d48"
    ]
  }),
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/brand-palette",
    method="PUT",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"], "Content-Type": "application/json"},
    data=json.dumps({"colors": ["#0f766e", "#F59E0B", "#e11d48"]}).encode(),
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "colors": [
    "#0F766E",
    "#F59E0B",
    "#E11D48"
  ]
}
```

Can also be refused with [forbidden_plan](https://chartjuice.com/docs/limits/#forbidden_plan).

#### Get colors from a website

POST /brand-palette/extract

The brand colors a website uses, read from its page and stylesheets. Nothing is saved: look at the colors, then save the ones you want with [`set_brand_palette`](https://chartjuice.com/docs/api/#set_brand_palette). Needs the Pro plan, and an account can look up 20 websites a day.

| Field | Type | What it is |
| --- | --- | --- |
| `url` in the body, required | string | A website address, like `example.com`. |

curl:

```bash
curl -X POST "https://chartjuice.com/api/v1/brand-palette/extract" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "chartjuice.com"
}'
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/brand-palette/extract', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "url": "chartjuice.com"
  }),
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/brand-palette/extract",
    method="POST",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"], "Content-Type": "application/json"},
    data=json.dumps({"url": "chartjuice.com"}).encode(),
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "colors": [
    "#5D55FA",
    "#102A43",
    "#0D009A",
    "#9595FF",
    "#4B42B9",
    "#9D97E2",
    "#C10007",
    "#9FB3C8"
  ],
  "added": 0
}
```

Can also be refused with [forbidden_plan](https://chartjuice.com/docs/limits/#forbidden_plan).

### Account

#### Get the account

GET /account

The plan, how many charts are saved against the plan's limit, and what is left of the request, render and image allowances. Check it before making many charts or images, and to explain a [`chart_limit_reached`](https://chartjuice.com/docs/limits/#chart_limit_reached), [`render_limit_reached`](https://chartjuice.com/docs/limits/#render_limit_reached) or [`image_limit_reached`](https://chartjuice.com/docs/limits/#image_limit_reached) refusal.

curl:

```bash
curl "https://chartjuice.com/api/v1/account" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY"
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/account', {
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/account",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "plan": "free",
  "saved_charts": {
    "used": 3,
    "limit": 5
  },
  "requests": {
    "limit": 60,
    "left": 59,
    "resets_in_seconds": 42
  },
  "renders": {
    "limit": 100,
    "left": 96,
    "resets_in_seconds": 35700
  },
  "image_renders": {
    "limit": 100,
    "left": 88,
    "resets_in_seconds": 1769700
  }
}
```

### Reference

#### List chart types

GET /chart-types

Every chart type: what it is for, the data it needs (one column per role), and a short guide to its options. Read it to choose a chart for some data, then make it with [`create_chart`](https://chartjuice.com/docs/api/#create_chart).

curl:

```bash
curl "https://chartjuice.com/api/v1/chart-types" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY"
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/chart-types', {
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/chart-types",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "chart_types": [
    {
      "type": "pie",
      "label": "Pie chart",
      "use_for": "Show parts of a whole",
      "roles": [
        {
          "role": "category",
          "label": "Slices",
          "numeric": false,
          "required": true
        },
        {
          "role": "value",
          "label": "Value",
          "numeric": true,
          "required": true
        }
      ],
      "guide": "Show parts of a whole. Roles: category (slice names) + value (one number column)."
    }
  ]
}
```

#### List examples

GET /examples

The gallery of example charts, each with sample data and a finished design. Pass an id as `example` to [`create_chart`](https://chartjuice.com/docs/api/#create_chart) to start from one.

| Field | Type | What it is |
| --- | --- | --- |
| `type` in the query, optional | "bar" \| "line" \| "pie" \| "scatter" \| "histogram" \| "boxplot" \| "dotplot" \| "sankey" \| "radar" \| "heatmap" | Only examples of this chart type. |

curl:

```bash
curl "https://chartjuice.com/api/v1/examples?type=bar" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY"
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/examples?type=bar', {
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/examples?type=bar",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "examples": [
    {
      "id": "bar-grouped",
      "type": "bar",
      "label": "Double bar graph",
      "description": "Compare two series side by side, like this year against last year."
    }
  ]
}
```

#### List palettes

GET /palettes

The preset color palettes. Apply one to a chart with the `setPalette` op and its colors.

curl:

```bash
curl "https://chartjuice.com/api/v1/palettes" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY"
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/palettes', {
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/palettes",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "palettes": [
    {
      "id": "juice",
      "name": "Juice",
      "colors": [
        "#0C008C",
        "#2D1FF5",
        "#5D55FA",
        "#8888FC",
        "#C4C6FF"
      ],
      "light": false
    }
  ]
}
```

#### Get the op catalog

GET /ops

Every op [`apply_ops`](https://chartjuice.com/docs/api/#apply_ops) takes, with its fields: as a compact text reference, and as a JSON Schema per op. Read it once before editing charts.

curl:

```bash
curl "https://chartjuice.com/api/v1/ops" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY"
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/ops', {
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/ops",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "catalog": "## Ops (pass a list to apply_ops; all must succeed or none apply)\n\nType and layout\n- {\"op\":\"setType\",\"type\":\"bar|line|pie|...\"}: data, mapping and style carry over.",
  "ops": [
    {
      "op": "setType",
      "schema": {
        "type": "object",
        "properties": {
          "op": {
            "type": "string",
            "const": "setType"
          },
          "type": {
            "type": "string",
            "enum": [
              "bar",
              "line",
              "pie"
            ]
          }
        },
        "required": [
          "op",
          "type"
        ],
        "additionalProperties": false
      }
    }
  ]
}
```

#### Look up the docs

GET /docs/search

Search Chartjuice's reference: which chart suits what, how to shape data for each chart, palettes, number formats, plans and limits. Use it when unsure, before guessing.

| Field | Type | What it is |
| --- | --- | --- |
| `q` in the query, required | string | What to look up, like `stacked bar` or `number formats`. |

curl:

```bash
curl "https://chartjuice.com/api/v1/docs/search?q=stacked+bar" \
  -H "Authorization: Bearer $CHARTJUICE_API_KEY"
```

JavaScript:

```javascript
const response = await fetch('https://chartjuice.com/api/v1/docs/search?q=stacked+bar', {
  headers: { Authorization: `Bearer ${process.env.CHARTJUICE_API_KEY}` },
})
const result = await response.json()
```

Python:

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://chartjuice.com/api/v1/docs/search?q=stacked+bar",
    headers={"Authorization": "Bearer " + os.environ["CHARTJUICE_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
```

Answer

```json
{
  "results": [
    {
      "title": "Bar charts",
      "text": "Compare amounts across categories. Roles: category (labels) + values (one or more number columns; each becomes a series)."
    }
  ]
}
```

---

This is https://chartjuice.com/docs/api/ as Markdown. Every docs page is listed at https://chartjuice.com/llms.txt.
