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+jsonper 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_requiredresponse 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"
}
languageis optional. The API tries two strategies in order:
- Filename extension — canonical for each lang plus common aliases (
.mjs/.cjs→ javascript,.yml→ yaml,.sh/.bash→ bash, etc.).- 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
filenamewith a real extension; fall back to explicitlanguageonly when neither approach suffices (e.g..tsx/.jsxcan't be distinguished from.ts/.jswithout the filename).
Response — default (application/json)
{
"id": "eTXDd4oFWLsBuVD3Tx5Gqw",
"url": "https://codeanimate.dev/api/embed/eTXDd4oFWLsBuVD3Tx5Gqw.svg",
"markdown": "",
"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": "",
"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.