# Enqueue a render
Source: https://docs.kodisc.com/api-reference/endpoint/enqueue-render
POST /api/v2/render
Submits a Manim scene for rendering and returns immediately with a `jobId`. The job runs asynchronously on Kodisc's render fleet.
A successful enqueue reserves a minimum number of credits up-front; the final cost is settled (and any unused credits refunded) when the render completes. If your balance is below the minimum reserve, the request fails with `402`.
Track progress by polling [`GET /api/v2/render/{jobId}`](/api-reference/endpoint/get-render) or by configuring a webhook (see [Webhooks](/webhooks)).
# Get account & credits
Source: https://docs.kodisc.com/api-reference/endpoint/get-me
GET /api/v2/me
Returns the user and key behind the credentials on this request, plus the live credit balance. Useful as a health check while wiring up an integration, or to show remaining credits inside your own dashboard.
# Get a render job
Source: https://docs.kodisc.com/api-reference/endpoint/get-render
GET /api/v2/render/{jobId}
Returns the current state of a render job. Poll this endpoint until `status` is `completed` or `failed`, or use a webhook to be notified instead.
Jobs are scoped to the authenticated user — requesting another user's `jobId` returns `404`.
# API reference
Source: https://docs.kodisc.com/api-reference/introduction
Base URL, conventions, and the three endpoints that make up the Kodisc API.
The Kodisc public API has three endpoints. They live under `/api/v2/` on the production host and authenticate with a Bearer token.
```
https://kodisc.com/api/v2
```
Download the full OpenAPI 3.1 spec that powers this reference.
## Endpoints
| Method | Path | Purpose |
| ------ | -------------------------------------------------------------- | ------------------------------------------------ |
| `POST` | [`/api/v2/render`](/api-reference/endpoint/enqueue-render) | Enqueue a render job |
| `GET` | [`/api/v2/render/{jobId}`](/api-reference/endpoint/get-render) | Poll a job's status and result |
| `GET` | [`/api/v2/me`](/api-reference/endpoint/get-me) | Inspect the authenticated key and credit balance |
## Conventions
* **Authentication.** Every request requires `Authorization: Bearer kdsc_live_…`. See [Authentication](/authentication).
* **Content type.** Requests and responses are JSON. Always send `Content-Type: application/json` on `POST`.
* **Timestamps.** All timestamps are ISO 8601 in UTC, e.g. `2026-04-28T17:14:02.118Z`.
* **IDs.** Treat all IDs (`jobId`, `keyId`, `userId`) as opaque strings.
* **Versioning.** This is `v2`. Breaking changes ship as `v3` at a new path; the existing `/api/v2/*` surface stays stable.
* **Errors.** Non-`2xx` responses always include an `error` field. See [Errors](/errors).
# Authentication
Source: https://docs.kodisc.com/authentication
API keys, Bearer tokens, rotation, and revocation.
Every request to `/api/v2/*` is authenticated with an API key sent as an HTTP Bearer token.
```http theme={"system"}
Authorization: Bearer kdsc_live_<48 hex chars>
```
Requests without a valid `Authorization` header — or with a key that's been revoked — get a [`401 Unauthorized`](/errors#401-unauthorized).
## Key format
Kodisc API keys look like:
```
kdsc_live_9c1d3f2e8b6a4d7c0f5e2a1b8c4d7e9f6a3b1c0d8e5f2a4b
```
* Prefix `kdsc_live_` marks the key as a live API credential.
* The remaining 48 characters are random hex.
* The first 16 characters (e.g. `kdsc_live_9c1d`) are the **display prefix** — safe to log, show in your dashboard, and refer to in support tickets. The full key is **not** safe to log.
## Creating a key
1. Open the [developer dashboard](https://kodisc.com/developer/keys).
2. Click **New key**, name it something you'll recognize later (e.g. `prod-render-worker`).
3. Optionally set a **default webhook URL** — every job created with this key will deliver to that URL unless overridden per-request (see [Webhooks](/webhooks)).
4. Copy the full key. It is **shown exactly once**. If you lose it, revoke the key and issue a new one.
When you create a key, Kodisc also generates a **webhook signing secret** (`whsec_…`). It never appears in any API response and is unique to that key — see [verifying signatures](/webhooks#verifying-signatures).
Store keys in a secret manager. Never commit them to git, ship them in a frontend bundle, or paste them into shared documents. The full plaintext is only retrievable at creation time — Kodisc only ever stores a SHA-256 hash.
## Using a key
Send the key in the `Authorization` header on every request:
```bash curl theme={"system"}
curl https://kodisc.com/api/v2/me \
-H "Authorization: Bearer $KODISC_KEY"
```
```ts node theme={"system"}
const res = await fetch("https://kodisc.com/api/v2/me", {
headers: { Authorization: `Bearer ${process.env.KODISC_KEY}` },
});
const me = await res.json();
```
```python python theme={"system"}
import os, requests
res = requests.get(
"https://kodisc.com/api/v2/me",
headers={"Authorization": f"Bearer {os.environ['KODISC_KEY']}"},
)
me = res.json()
```
Tip: hit [`GET /api/v2/me`](/api-reference/endpoint/get-me) right after wiring up auth — it confirms the key is valid and returns your live credit balance.
## Rotating the webhook secret
If you suspect your webhook signing secret has leaked, rotate it from the [developer dashboard](https://kodisc.com/developer/keys). The old secret is invalidated immediately; subsequent webhook deliveries are signed with the new one. The API key itself is unaffected.
## Revoking a key
Revoke any key from the [developer dashboard](https://kodisc.com/developer/keys). Revocation is immediate — the next request with that key returns `401`. There is no undo; create a new key if you change your mind.
Best practice: keep at least two keys in production (e.g. one per worker pool) so you can rotate without downtime.
# Errors & rate limits
Source: https://docs.kodisc.com/errors
Status codes, the credit model, and how to recover.
Every error response is JSON with at least an `error` field, and often a `message` for humans:
```json theme={"system"}
{
"error": "insufficient_credits",
"message": "Not enough credits to start a render. Top up at /developer/billing."
}
```
## Status codes
| Status | Meaning | What to do |
| ------ | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Request body or parameters invalid (bad `className`, unknown `quality`, non-`https://` `webhookUrl`, malformed JSON, …). | Read `error`; fix the field it names. These are deterministic — don't retry without changing the request. |
| `401` | Missing, malformed, revoked, or unknown API key. | Check that `Authorization: Bearer kdsc_live_…` is set and matches a live key in the [dashboard](https://kodisc.com/developer/keys). |
| `402` | Not enough credits to start the render. | Top up from [billing](https://kodisc.com/developer/billing) or wait until your monthly free allotment resets. |
| `404` | The `jobId` doesn't exist or doesn't belong to this key's user. | Confirm the ID; jobs are scoped to the user that created them. |
| `5xx` | Transient server-side failure. | Retry with exponential backoff. |
A render that *fails* during execution (bad Manim code, runtime error) still returns `200` from `GET /api/v2/render/{jobId}` — the failure surfaces as `status: "failed"` with `error` populated. HTTP `5xx` is reserved for Kodisc-side problems, not your scene's.
## The credit model
Credits are the unit Kodisc bills against. Each render consumes credits proportional to its wall-clock render time and quality preset.
The lifecycle of credits during a render:
On `POST /api/v2/render`, Kodisc reserves a minimum amount of credits up-front. If your balance is below the reserve, the call fails with `402` immediately — no job is created.
When the render finishes, Kodisc computes the actual cost from `durationMs` and `quality`. If the actual cost exceeds the reserve, the difference is charged. If you don't have enough to cover it, the job is marked `failed` and the reserve is refunded.
If the actual cost is below the reserve, the difference is automatically returned to your balance. Failed jobs refund the full reserve.
Credit balance is visible at any time via [`GET /api/v2/me`](/api-reference/endpoint/get-me) — the response splits **paid** (purchased credits) from **free** (your plan's monthly allowance).
## Common 400 messages
| `error` | Cause |
| -------------------------------------------------------- | ------------------------------------------------------ |
| `JSON body required` | Body wasn't valid JSON. |
| `code is required` | `code` was missing or empty. |
| `className must be a valid Python identifier` | `className` didn't match `^[A-Za-z_][A-Za-z0-9_]*$`. |
| `quality must be one of: low, medium, high, twok, fourk` | Unknown `quality`. |
| `aspectRatio must be one of: 16:9, 9:16` | Unknown `aspectRatio`. |
| `fps must be a positive integer` | `fps` wasn't a positive integer. |
| `webhookUrl must be an https:// URL` | `webhookUrl` was set but didn't start with `https://`. |
## Retry strategy
* `400` and `401` are **client errors** — retrying without changes won't help.
* `402` clears as soon as your balance increases; retry after topping up or after the next free-credit refresh.
* `404` is permanent for the given `jobId`.
* `5xx` and network errors are safe to retry; use exponential backoff (e.g. 1s → 2s → 4s, up to 5 attempts).
# Welcome
Source: https://docs.kodisc.com/introduction
Render Manim scenes from your own apps with the Kodisc API.
Kodisc turns [Manim](https://www.manim.community) Python code into rendered MP4 videos, JPEG thumbnails, and WebVTT captions — without you running the toolchain. Send a `Scene`, get back a URL.
The API is intentionally small: enqueue a render, poll (or webhook) for the result, top up credits when you need to.
Render your first scene end-to-end in under five minutes.
Three endpoints, fully typed, with live Try-It panels.
`kdsc_live_*` keys, Bearer auth, key rotation.
Get pinged the moment a render is ready — with verifiable signatures.
## How it works
`POST /api/v2/render` with your `Scene` source and the class name to render. You get back a `jobId` immediately — renders run asynchronously.
Either poll `GET /api/v2/render/{jobId}` until `status` is `completed`, or configure a [webhook](/webhooks) to be notified the moment it lands.
Completed jobs return public URLs to an MP4 video, a thumbnail image, and (when applicable) a captions file. Download or hot-link them.
## What's not covered here
This site documents the **public** Kodisc API only. The in-app surfaces — project management, the generation chat, narration uploads, internal tRPC procedures — are intentionally not part of the public contract and aren't documented here. If you need something that isn't in the reference, [let us know](https://kodisc.com).
# MCP Server
Source: https://docs.kodisc.com/mcp
Use the Kodisc MCP server to render Manim scenes directly from AI coding tools like Claude Code and Cursor.
The Kodisc MCP (Model Context Protocol) server lets AI coding assistants enqueue renders, check job status, and search documentation without leaving your editor.
## Installation
Run the following command to add the Kodisc MCP server to Claude Code:
```bash theme={"system"}
claude mcp add --transport http kodisc https://mcp.kodisc.com/mcp
```
Or add it manually to your `~/.claude/settings.json`:
```json theme={"system"}
{
"mcpServers": {
"kodisc": {
"type": "http",
"url": "https://mcp.kodisc.com/mcp"
}
}
}
```
Open **Cursor Settings → MCP** and add a new server:
```json theme={"system"}
{
"kodisc": {
"type": "http",
"url": "https://mcp.kodisc.com/mcp"
}
}
```
Any MCP-compatible client can connect using the HTTP transport:
```
https://mcp.kodisc.com/mcp
```
Refer to your client's documentation for how to register an HTTP MCP server.
## Authentication
Pass your Kodisc API key as a Bearer token in the `Authorization` header when your client connects. Most MCP clients let you set custom headers in the server config:
```json theme={"system"}
{
"kodisc": {
"type": "http",
"url": "https://mcp.kodisc.com/mcp",
"headers": {
"Authorization": "Bearer kdsc_live_..."
}
}
}
```
Don't have a key? [Get one from the dashboard](https://kodisc.com/developer/keys).
## What you can do
Once connected, your AI assistant can:
* **Enqueue renders** — submit Manim scene code and get back a job ID
* **Check render status** — poll a job until it completes and retrieve asset URLs
* **Search documentation** — query the Kodisc docs without leaving the editor
Full details are at [docs.kodisc.com/mcp](https://docs.kodisc.com/mcp).
# Quickstart
Source: https://docs.kodisc.com/quickstart
Render a scene end-to-end with curl in under five minutes.
This walkthrough goes from "no key" to a downloaded MP4 using nothing but `curl`. If you'd rather see typed examples, the [API reference](/api-reference/introduction) ships per-endpoint snippets in JavaScript and Python.
Open the [developer dashboard](https://kodisc.com/developer/keys), click **New key**, give it a name, and copy the `kdsc_live_…` value. The plaintext is only shown once — store it somewhere safe.
Want details on key format, rotation, and the optional default webhook URL? See [Authentication](/authentication).
`POST` your Manim code to `/api/v2/render`. The response is immediate — you get a `jobId`, not a video.
```bash theme={"system"}
export KODISC_KEY="kdsc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
curl -sS https://kodisc.com/api/v2/render \
-H "Authorization: Bearer $KODISC_KEY" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"code": "from manim import *\n\nclass HelloKodisc(Scene):\n def construct(self):\n self.play(Write(Text(\"Hello, Kodisc\")))\n self.wait(1)\n",
"className": "HelloKodisc",
"quality": "medium",
"aspectRatio": "16:9"
}
JSON
```
```json Response (202) theme={"system"}
{
"jobId": "clx9f0a1b0000abcd1234efgh",
"status": "queued",
"endpoint": "render"
}
```
Hit `GET /api/v2/render/{jobId}` every few seconds. When `status` flips to `completed`, the `result` object is populated.
```bash theme={"system"}
curl -sS https://kodisc.com/api/v2/render/clx9f0a1b0000abcd1234efgh \
-H "Authorization: Bearer $KODISC_KEY"
```
```json Response (200) theme={"system"}
{
"jobId": "clx9f0a1b0000abcd1234efgh",
"endpoint": "render",
"status": "completed",
"creditsCost": 42,
"durationMs": 18420,
"result": {
"video": "https://cdn.kodisc.com/r/clx9f0a1b0000abcd1234efgh/video.mp4",
"thumbnail": "https://cdn.kodisc.com/r/clx9f0a1b0000abcd1234efgh/thumb.jpg",
"captions": "https://cdn.kodisc.com/r/clx9f0a1b0000abcd1234efgh/captions.vtt"
},
"error": null,
"metadata": null,
"createdAt": "2026-04-28T17:14:02.118Z",
"completedAt": "2026-04-28T17:14:20.539Z"
}
```
In production, prefer [webhooks](/webhooks) to polling. Set `webhookUrl` once on the key and forget about it.
The URLs in `result` are public and ready to embed, hot-link, or download.
```bash theme={"system"}
curl -O https://cdn.kodisc.com/r/clx9f0a1b0000abcd1234efgh/video.mp4
```
Need to know how much you have left to spend? `GET /api/v2/me` returns the current credit balance.
## What to read next
Replace polling with a single signed POST per terminal state.
`400`, `401`, `402`, `404` — what each one means and how to recover.
# Webhooks
Source: https://docs.kodisc.com/webhooks
Get notified the moment a render finishes, with verifiable signatures.
Webhooks are how you avoid polling. When a render reaches a terminal state — `completed` or `failed` — Kodisc `POST`s a signed JSON payload to a URL you control.
## When webhooks fire
A delivery is attempted exactly once per terminal state transition for jobs that have a `webhookUrl`. Two ways to set one:
* **Default for the key**: configure a `webhookUrl` on the API key in the [developer dashboard](https://kodisc.com/developer/keys). Every job enqueued with that key inherits it.
* **Per request**: pass `webhookUrl` in the body of `POST /api/v2/render` to override the key's default for that single job. Pass `null` to opt the job out of webhooks entirely.
If neither is set, no webhook is delivered — you're expected to poll.
## Payload
The body is JSON. Field shapes mirror the [`GET /api/v2/render/{jobId}`](/api-reference/endpoint/get-render) response.
```json theme={"system"}
{
"jobId": "clx9f0a1b0000abcd1234efgh",
"endpoint": "render",
"status": "completed",
"createdAt": "2026-04-28T17:14:02.118Z",
"completedAt": "2026-04-28T17:14:20.539Z",
"durationMs": 18420,
"creditsCost": 42,
"metadata": { "projectId": "proj_42" },
"result": {
"video": "https://cdn.kodisc.com/r/clx9f0a1b0000abcd1234efgh/video.mp4",
"thumbnail": "https://cdn.kodisc.com/r/clx9f0a1b0000abcd1234efgh/thumb.jpg",
"captions": "https://cdn.kodisc.com/r/clx9f0a1b0000abcd1234efgh/captions.vtt"
},
"error": null
}
```
For failed jobs, `status` is `"failed"`, `result` is `null`, and `error` carries a human-readable message.
## Headers
Each delivery includes:
| Header | Value |
| ------------------ | -------------------------------------------- |
| `content-type` | `application/json` |
| `kodisc-event` | `completed` or `failed` |
| `kodisc-signature` | `t=,v1=` — see below |
## Verifying signatures
The signature lives in `kodisc-signature` and looks like `t=1714326842,v1=4f8a…`. To verify:
1. Split the header on `,` and parse out `t` (timestamp) and `v1` (HMAC). When splitting each `key=value` pair, only split on the **first** `=` — values like base64-encoded HMACs may contain `=` padding.
2. Build the **signed string** as `${t}.${rawBody}` — the literal request body, byte-for-byte, before any JSON parsing.
3. Compute `HMAC-SHA256(signed_string, webhook_secret)` and hex-encode it.
4. Compare against `v1` using a constant-time equality check.
5. **Always verify the timestamp is within 5 minutes of now to prevent replay of captured deliveries.** Without this check, an attacker who once captures a valid signed request can replay it forever.
Reject the request if anything doesn't match.
```ts node theme={"system"}
import crypto from "node:crypto";
const TOLERANCE = 5 * 60; // 5 minutes
export function verifyKodiscSignature(
rawBody: string,
header: string | null,
secret: string,
): boolean {
if (!header) return false;
const parts = Object.fromEntries(
header.split(",").map((p) => {
// Only split on the first `=` so base64-padded values survive intact.
const [k, ...rest] = p.trim().split("=");
const v = rest.join("=");
return [k, v] as [string, string];
}),
);
const timestamp = parts.t;
const received = parts.v1;
if (!timestamp || !received) return false;
// Reject deliveries whose timestamp is outside the freshness window —
// prevents replay of captured signed requests.
if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > TOLERANCE) {
return false;
}
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(received, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```
```python python theme={"system"}
import hmac
import time
from hashlib import sha256
TOLERANCE = 5 * 60 # 5 minutes
def verify_kodisc_signature(raw_body: bytes, header: str | None, secret: str) -> bool:
if not header:
return False
parts = dict(p.strip().split("=", 1) for p in header.split(","))
timestamp = parts.get("t")
received = parts.get("v1")
if not timestamp or not received:
return False
# Reject deliveries whose timestamp is outside the freshness window —
# prevents replay of captured signed requests.
if abs(int(time.time()) - int(timestamp)) > TOLERANCE:
return False
signed = f"{timestamp}.{raw_body.decode()}".encode()
expected = hmac.new(secret.encode(), signed, sha256).hexdigest()
return hmac.compare_digest(expected, received)
```
Verify against the **raw** request body, not a re-serialized version. Frameworks like Express or FastAPI may JSON-parse the body and lose key ordering — capture the bytes before that happens.
## Retries
Kodisc treats a delivery as successful when your endpoint returns a `2xx` within 10 seconds. Otherwise, it retries up to **3 more times** with these delays from the original attempt:
| Attempt | Delay before sending |
| ------- | -------------------- |
| 1 | immediate |
| 2 | 30 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
After the fourth attempt, Kodisc stops trying. The job's last delivery status (HTTP code, or `0` if the request never connected) and number of attempts are visible in the dashboard, so you can replay manually if needed.
Design your handler to be **idempotent** — even though Kodisc doesn't intentionally double-deliver, network edges happen. Treat `jobId` as the dedupe key.
## Recommended response
Return `2xx` as soon as you've persisted the event. Do the heavy work (downloading the video, updating your DB, notifying users) asynchronously. Slow handlers eat into your 10-second window and can trigger spurious retries.
## Testing locally
Kodisc requires webhook URLs to start with `https://`, and deliveries are made from Kodisc's servers — so `http://localhost` won't work. The solution is a tunnel: a tool that gives your local server a public HTTPS URL that Kodisc can reach.
### Option 1: ngrok
The most widely used option. [Install ngrok](https://ngrok.com/download), then run:
```bash theme={"system"}
ngrok http 3000
```
ngrok prints a URL like `https://abc123.ngrok-free.app`. Use that as your webhook URL when calling the API or configuring your key.
### Option 2: Cloudflare Tunnel
No account required for quick tunnels. [Install cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/), then run:
```bash theme={"system"}
cloudflared tunnel --url http://localhost:3000
```
Cloudflare prints a `https://` URL you can use immediately.
### Option 3: VS Code port forwarding
If you're using VS Code with a GitHub account, open the **Ports** panel, right-click your local port, and select **Make Public**. VS Code copies an `https://` forwarding URL to your clipboard.
***
Once your tunnel is running, pass its URL as `webhookUrl` in your render request:
```bash theme={"system"}
curl -X POST https://api.kodisc.com/api/v2/render \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"code": "...",
"className": "MyScene",
"webhookUrl": "https://abc123.ngrok-free.app/webhook"
}'
```
Your local handler will receive the delivery as if it were in production.