DOCS

API reference

API reference

All endpoints are served from https://codeanimate.dev. Responses are either:

  • The requested image (image/svg+xml, image/png, image/gif), or
  • application/problem+json per RFC 7807 for errors.

CORS is open (Access-Control-Allow-Origin: *) on the public endpoints — the embed URL is meant to be fetched from anywhere.

POST /api/snippets

Pro tier required. Free users receive a 402 pro_required response pointing at /pricing. Permalink sharing is a Pro feature; free-tier users still get one-shot client-side PNG/GIF exports from the editor.

Store a composition and get back a short, content-addressable embed URL.

Authentication: either

  • Authorization: Bearer ca_live_… (API key), or
  • Clerk session cookie (from the editor UI).

Request body

{
  "snapshot": {
    "code": "def greet(name):\n    print(f\"Hello, {name}!\")",
    "language": "python",
    "themeId": "dark-plus",
    "fontSize": 16,
    "titleBar": "macos",
    "filename": "hello.py",
    "showLineNumbers": true,
    "timing": { "typingSpeed": 22, "endPause": 1.5 },
    "shadowStrength": 50,
    "stageWidth": 768
  },
  "name": "hero demo"
}

language is optional. The API tries two strategies in order:

  1. Filename extension — canonical for each lang plus common aliases (.mjs/.cjs → javascript, .yml → yaml, .sh/.bash → bash, etc.).
  2. Content heuristic — weighted regex via @speed-highlight/core/detect (~4 KB). Covers python, javascript, typescript, go, rust, java, c/cpp, sql, html, css, json, yaml, bash, markdown.

Detection is deliberately conservative — it returns null (and the API returns 400) when the signal is ambiguous, rather than guessing wrong. For the best results pass filename with a real extension; fall back to explicit language only when neither approach suffices (e.g. .tsx/.jsx can't be distinguished from .ts/.js without the filename).

Response — default (application/json)

{
  "id": "eTXDd4oFWLsBuVD3Tx5Gqw",
  "url": "https://codeanimate.dev/api/embed/eTXDd4oFWLsBuVD3Tx5Gqw.svg",
  "markdown": "![codeanimate.dev demo](https://codeanimate.dev/api/embed/eTXDd4oFWLsBuVD3Tx5Gqw.svg)",
  "html": "<img src=\"https://codeanimate.dev/api/embed/eTXDd4oFWLsBuVD3Tx5Gqw.svg\" alt=\"codeanimate.dev demo\" />"
}

The id is a deterministic hash of the snapshot — re-uploading the same composition returns the same id and URL. Safe to retry.

?output= variants

Shell scripts that don't want to parse JSON can request a different shape:

?output= Content-Type Body
(omitted) application/json { id, url, markdown, html }
url text/plain just the URL
markdown text/markdown just the Markdown snippet
svg image/svg+xml the rendered SVG (same bytes the URL serves)

Examples:

# Create + get back just the URL
curl -X POST 'https://codeanimate.dev/api/snippets?output=url' \
  -H "Authorization: Bearer ca_live_..." -H "Content-Type: application/json" \
  -d @snapshot.json
# → https://codeanimate.dev/api/embed/eTXDd4oFWLsBuVD3Tx5Gqw.svg

# Create + download the SVG directly (also persists the snippet)
curl -X POST 'https://codeanimate.dev/api/snippets?output=svg' \
  -H "Authorization: Bearer ca_live_..." -H "Content-Type: application/json" \
  -d @snapshot.json -o demo.svg

# Create + paste straight into a README
curl -X POST 'https://codeanimate.dev/api/snippets?output=markdown' \
  -H "Authorization: Bearer ca_live_..." -H "Content-Type: application/json" \
  -d @snapshot.json | pbcopy

GET /api/snippets/<id>

Returns metadata + 12 months of hit history for one snippet. Ownership check — a caller can only see snippets they (or one of their API keys) created.

Response

{
  "id": "eTXDd4oFWLsBuVD3Tx5Gqw",
  "name": "codeanimate.dev demo",
  "createdAt": 1776622610538,
  "lastRenderedAt": null,
  "url": "https://codeanimate.dev/api/embed/eTXDd4oFWLsBuVD3Tx5Gqw.svg",
  "markdown": "![codeanimate.dev demo](https://codeanimate.dev/api/embed/eTXDd4oFWLsBuVD3Tx5Gqw.svg)",
  "hits": {
    "thisMonth": 42,
    "history": [
      { "month": "2025-05", "hits": 0 },
      { "month": "2025-06", "hits": 0 },
      /* … 12 months … */
      { "month": "2026-04", "hits": 42 }
    ]
  },
  "snapshot": { "code": "...", "language": "typescript", "...": "..." }
}

PATCH /api/snippets/<id>

Rename a snippet. { "name": "New name" }. Returns { id, name }.

DELETE /api/snippets/<id>

Removes the D1 row. Subsequent /api/embed/<id>.* requests return a graceful 404 SVG. Cached R2 renders stay (harmlessly) until their natural TTL.

GET /p/<id>

The human-facing presentation page — an HTML wrapper around the animated SVG, plus a Copy-code button and Open Graph meta tags so Slack / X / LinkedIn / Discord render a preview card when the URL is pasted. This is what the editor's Copy link action produces.

  • Fully public (no auth needed to view).
  • OG image lives at /p/<id>/opengraph-image (PNG, 1200×630) and is wired into the page's <meta property="og:image"> automatically.

GET /api/embed/<id>.<ext>

The raw-image route — for when you want the animation to play inline inside an <img> tag (README, docs, Notion) rather than behind a link. Returns the rendered image directly. Cacheable for a year; changes to the snippet yield a new id, a new URL.

Path parameters

Name Description
<id> Snippet id from POST /api/snippets.
<ext> svg, png, or gif.

Query parameters

Name Required Description
k API key. Anonymous requests hit a per-IP cap.

Response headers

  • Content-Type: format-specific.
  • Cache-Control: public, max-age=31536000, immutable (content-addressable URL).
  • X-Tier: free \| pro \| anon — informational, indicates which plan counted this render.

Errors (returned as image/svg+xml so README layout never breaks):

  • Quota exceeded → a polite "Upgrade" card.
  • Snippet not found → a "Not found" card.

POST /api/keys · GET /api/keys · DELETE /api/keys/<id>

API key CRUD. Clerk session required. See the dashboard UI for the web flow.

POST body: { "name": "README dogfood" }

POST response (key returned exactly once):

{
  "id": "uuid",
  "name": "README dogfood",
  "prefix": "a1b2c3",
  "key": "ca_live_a1b2c3_k9X3mQ8vN2pL5rT7yU4wZ6hJ",
  "createdAt": 1713556790000
}

GET /api/usage

Current-month render count for the signed-in user.

{
  "month": "2026-04",
  "tier": "free",
  "renders": 3,
  "limit": 10,
  "remaining": 7
}

Error shape

All errors are returned as application/problem+json:

{
  "type": "urn:code-animator:quota_exceeded",
  "title": "Quota exceeded",
  "status": 429
}
type suffix Status Meaning
missing_key 401 No auth provided and the endpoint requires it.
invalid_key / revoked_key 401 Key is malformed, unknown, or revoked.
invalid_share / share_too_long 400 / 413 Short-form payload unusable.
invalid_snapshot 400 Snapshot failed schema validation.
format_unsupported 415 Not svg/png/gif. MP4/WebM are in-editor only.
quota_exceeded 429 Monthly cap reached.
rate_limited 429 Anonymous IP burst cap.
not_found 404 Snippet id doesn't exist.
internal 500 Something broke. Retry; file an issue if persistent.

Rate limits

Traffic class Limit
Anonymous per IP 20 / minute, 50 / month.
Free tier (signed in) 10 / month.
Pro tier Unlimited.

Quota decrement happens on the edge via Cloudflare KV; counters flush to our durable ledger (D1) asynchronously, so request latency is unaffected.


Back to the embedding guide.