Chartsmith

Reference

API docs

One endpoint that matters: post a chart spec, get a rendered image at a stable URL. 1 credit per chart on the credits rail, $0.012 in USDC on the keyless one.

Quickstart

Mint a key at /dashboard/api-keys, then post a spec. The call is synchronous — there is no job to poll and no upstream provider in the loop.

curl -s https://charts.ounie.com/api/charts \
  -H "Authorization: Bearer cht_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "bar",
    "title": "Revenue by quarter",
    "data": {
      "labels": ["Q1", "Q2", "Q3", "Q4"],
      "series": [{ "name": "Revenue", "values": [42, 58, 51, 74] }]
    },
    "theme": "light",
    "format": "png"
  }'

The response carries the signed URL, the value table, and what it cost:

{
  "ok": true,
  "chart": {
    "id": "0f0e…",
    "type": "bar",
    "format": "png",
    "width": 1200,
    "height": 675,
    "credits_spent": 1,
    "url": "https://ounie.s3.amazonaws.com/charts/…?X-Amz-Signature=…"
  },
  "url": "https://…",
  "table": {
    "columns": ["label", "Revenue"],
    "rows": [["Q1", "42"], ["Q2", "58"], ["Q3", "51"], ["Q4", "74"]]
  },
  "advisories": [],
  "credits_spent": 1
}

The URL is signed for 24 hours. Reading the chart back later with GET /api/charts/<id> mints a fresh one and costs nothing.

The chart spec

One shape, used identically by REST, MCP and x402. Everything except type and data has a default.

{
  "type":  "bar | line | area | pie | donut | scatter | stackedBar | horizontalBar",
  "data": {
    "labels": ["Q1", "Q2"],              // every type except scatter
    "series": [
      { "name": "Revenue", "values": [42, 58] },        // categorical
      { "name": "Latency", "points": [{"x":1,"y":2}] }  // scatter only
    ]
  },
  "title":       "Revenue by quarter",   // optional
  "subtitle":    "FY2026",               // optional
  "theme":       "light",                // light | dark | brand      (default light)
  "width":       1200,                   // 320–2400                  (default 1200)
  "height":      675,                    // 200–2400                  (default 675)
  "format":      "png",                  // png | svg                 (default png)
  "xLabel":      "Quarter",              // optional, ignored by pie/donut
  "yLabel":      "USD",                  // optional, ignored by pie/donut
  "valueFormat": "number",               // number | compact | currency | percent
  "currency":    "USD",                  // when valueFormat is currency
  "legend":      true                    // force on/off; default on for 2+ series
}

valueFormat controls how numbers print on ticks and labels: number, compact, currency, percent. Note that percent appends a % to the value as given— 42 prints as “42%”. It does not multiply by 100, because guessing whether you meant 0.42 or 42 is how charts start lying.

Send null inside values for a missing point. On a line the path breaks there rather than being interpolated across the gap.

REST

POST/api/chartsrender a chart · 1 credit
GET/api/chartslist your charts · free
GET/api/charts/{id}read one back, fresh signed URL · free
DELETE/api/charts/{id}delete a chart and its image · free
GET/api/creditsyour spendable credits · free
GET/api/pricingpublic price list · no auth

Authenticate with Authorization: Bearer cht_live_…, or with ?api_key=cht_live_… when your client cannot set a header. A signed-in browser session works too — these are the same endpoints the dashboard uses.

The owner’s ounie.com ounie_live_… master key is also accepted, if it has been enabled for Ounie apps in ounie.com settings.

MCP

One HTTP MCP server at https://charts.ounie.com/api/mcp. It works with Claude, Cursor, ChatGPT, the AI SDK, and the Ounie AI Team.

{
  "mcpServers": {
    "chartsmith": {
      "type": "http",
      "url": "https://charts.ounie.com/api/mcp?api_key=cht_live_…"
    }
  }
}

The ?api_key= form is there because some hosts — the Ounie AI Team among them — cannot attach a static Authorization header to a manual HTTP MCP entry, so the key has to ride the URL.

  • render_chartdraw and store a chart · 1 credit
  • get_chartread one back with a fresh URL · free
  • list_chartslist yours · free
  • delete_chartdelete one · free
  • list_chart_typesevery form + a worked example · public
  • get_credit_balancespendable credits · free
  • get_pricingprices · public
  • whoamiwhich account this key belongs to

Credits are reserved server-side before anything renders, so an agent is refused, never overdrawn: a short wallet gets an insufficient_credits error naming the amount needed and where to top up.

x402 — pay per call, no account

For agents with no Ounie account. Post a spec to /api/x402/render; the first call answers 402 with payment terms, and you sign an EIP-3009 authorization and retry with an X-Payment header. $0.012 in USDC on Base, per chart.

# 1. quote — no payment header, spec is validated first
curl -s -X POST https://charts.ounie.com/api/x402/render \
  -H "Content-Type: application/json" \
  -d '{"type":"bar","data":{"labels":["a","b"],"series":[{"name":"n","values":[1,2]}]}}'
# → HTTP 402
# {"x402Version":1,"accepts":[{"scheme":"exact","network":"base",
#    "maxAmountRequired":"12000","payTo":"0x…","asset":"0x8335…",
#    "extra":{"name":"USD Coin","version":"2"}}]}

# 2. sign the EIP-3009 authorization, then retry with the header
curl -s -X POST https://charts.ounie.com/api/x402/render \
  -H "X-Payment: <base64 payload>" \
  -H "Content-Type: application/json" -d '{ … same body … }'

Two details worth knowing. The spec is validated before you are quoted, so a malformed chart is refused at 400 rather than 402. And payment is verified before the render and only settled after the chart exists — an on-chain settlement is final with no refund path, so nothing that can fail happens after the money moves.

An x402 chart belongs to nobody: there is no account to list it under, so keep the signed URL or download the image. Anonymous artifacts are swept after 30 days, long past the 24-hour life of the link.

Errors

400invalid_spec, length_mismatch, too_many_series, …nothing was charged
401unauthorizedno session, and no valid Bearer key
402insufficient_creditscarries required_credits + buy_credits_url
404not_foundno chart with that id on this account
413spec_too_largebody over 512 KB
502render_failedthe reserve was refunded pool-exact
503temporarily_unavailablestorage unreachable; not charged

Validation runs before any credit is reserved, so every 400 above is free. The message names the series that is wrong and what to send instead.

Limits and rules

typesbar · line · area · pie · donut · scatter · stackedBar · horizontalBar
themeslight · dark · brand
series1–8 (the palette is 8 slots and is never cycled)
pointsup to 500 per series
size320–2400 px on each axis
body512 KB
url life24 hours, re-signed free

Bars and areas always grow from a zero baseline; pie and donut refuse negative values, because a negative part has no share of a whole. Charts with two or more series always carry a legend, scatter series get distinct marker shapes as well as colours, and every response includes the value table — so nothing is readable by colour alone.

There is no free render endpoint. The dashboard preview draws in your browser using the same renderer, which is why getting a spec right costs nothing.

Something unclear, or missing from these docs?

Tell us →