Version 2026-09-12
Klemet API
Base URL https://klemet.app/api/v1. All requests and responses are JSON.
Overview
A render takes minutes, and no HTTP request should stay open that long, so the API is asynchronous:
POST /rendersaccepts your prompt and answers202at once with anid.GET /renders/{id}tells you where it is; whenstatusissucceeded,output.urldownloads the file.- Or give a
webhook_urland we call you when it is done.
Every render is an object with a state: queued → running → succeeded or failed. A failed render is refunded.
Quickstart
Submit a four-second render, then poll until it is done.
curl
export KLEMET_KEY=klm_live_… # 1. Submitcurl -s -X POST https://klemet.app/api/v1/renders \ -H "Authorization: Bearer $KLEMET_KEY" \ -H "content-type: application/json" \ -d '{"prompt":"A clay amphora on packed earth, warm afternoon light, slow push in.","seconds":4}'# → 202 {"id":"c2737017ed54","status":"queued",...} # 2. Read (repeat every few seconds until status is succeeded or failed)curl -s https://klemet.app/api/v1/renders/c2737017ed54 \ -H "Authorization: Bearer $KLEMET_KEY"# → {"status":"succeeded","output":{"url":"https://…"},"usd":0.014}Node
const KEY = process.env.KLEMET_KEY;const base = "https://klemet.app/api/v1";const headers = { authorization: `Bearer ${KEY}`, "content-type": "application/json" }; const submitted = await fetch(`${base}/renders`, { method: "POST", headers, body: JSON.stringify({ prompt: "A clay amphora on packed earth, warm light.", seconds: 4 }),}).then((r) => r.json()); let render = submitted;while (render.status === "queued" || render.status === "running") { await new Promise((r) => setTimeout(r, 5000)); render = await fetch(`${base}/renders/${submitted.id}`, { headers }).then((r) => r.json());}console.log(render.status, render.output?.url, render.usd);Python
import os, time, requests KEY = os.environ["KLEMET_KEY"]BASE = "https://klemet.app/api/v1"H = {"Authorization": f"Bearer {KEY}"} render = requests.post(f"{BASE}/renders", headers=H, json={ "prompt": "A clay amphora on packed earth, warm light.", "seconds": 4,}).json() while render["status"] in ("queued", "running"): time.sleep(5) render = requests.get(f"{BASE}/renders/{render['id']}", headers=H).json() print(render["status"], render.get("output", {}).get("url"), render.get("usd"))Submitting answers 202 in under a second. A short render reaches succeeded in about thirty seconds, and up to two minutes when the model has to load first.
Authentication
Create a key at klemet.app/dashboard/keys. It starts with klm_live_ and is shown once. Send it as a Bearer token on every request:
Authorization: Bearer klm_live_…Never put the key in a URL and never ship it to a browser: it spends your balance. Lost it? Revoke it and create another. We only store a hash, so nobody can read it back, not even us.
Claude and ChatGPT
Klemet is an MCP server, so you can add it to Claude or ChatGPT and ask for renders in plain language. There is no key to copy: you sign in to Klemet once, in your browser, and the assistant is connected to your account.
https://klemet.app/mcpIn Claude, open Settings, then Connectors, Add custom connector, and paste that address. In ChatGPT, add it as a connector the same way. A Klemet page opens, you confirm, and it is done.
The assistant then gets one tool per job: generate_video, generate_image, generate_speech and generate_music to start a render, get_job to follow it, list_models to read the catalogue with its prices, and show_credits to read your balance. Renders are billed to your wallet exactly as they are through the API, so ask for the price before you ask for the render.
List models
GET/models
The catalogue, as the site shows it. Open without a key, so you can read a price before you have an account. Filter with ?modality= (video, image, speech) and ?status= (live or soon).
{ "object": "list", "data": [ { "id": "lightricks/ltx-2.5", "object": "model", "name": "LTX-2.5", "publisher": "Lightricks", "modality": "video", "status": "live", "licence": "ltx-2.x-community-license-agreement", "weights_url": "https://huggingface.co/Lightricks/LTX-2.5", "pricing": { "unit": "/second", "usd": 0.0035 } } ], "has_more": false}status is live when the model renders today and soon when it is listed but not yet available. A soon model is refused with 400 and carries no price.
Create a render
POST/renders
Request body:
| Field | Type | Notes |
|---|---|---|
| model | string | A catalogue id, as GET /models returns it. Absent, you get LTX-2.5 in video. |
| prompt | string, required | What the engine reads. Up to 4,000 characters, and 40,000 in speech. English works best. |
| seconds | integer | Video 4 to 8, default 5. Music 5 to 180, default 30. |
| width, height | integer | Image only. 256 to 2048 px, rounded to a multiple of 64. Default 1024 each. The price is the one published on the model, per image, whatever the size. |
| aspect | string | 16:9, 9:16 or 1:1. Default 16:9. |
| resolution | string | 480p or 720p or 1080p. Default 480p. Video only, and only on the models whose page lists resolution tiers. |
| seed | integer | Fix it to reproduce a render. Random when absent. |
| webhook_url | string | We POST the finished render there. See Webhooks. |
Headers:
| Header | Notes |
|---|---|
| Authorization | Bearer klm_live_… (required) |
| Idempotency-Key | Optional. Retry safely. See Idempotency. |
| X-Klemet-Version | Optional. 2026-09-12 is the only version today. |
Response 202 Accepted, with a Location header pointing at the render:
{ "id": "c2737017ed54", "object": "render", "kind": "video", "model": "lightricks/ltx-2.5", "status": "queued", "created_at": "2026-09-12T15:04:05.000Z", "finished_at": null, "input": { "prompt": "…", "seconds": 4, "aspect": "16:9", "seed": 1837201 }, "output": null, "estimated_usd": 0.014, "usd": null, "error": null}The price is charged to your balance when you submit, and it does not move afterwards. Video is priced per second of output, music per minute, an image per image, and a voice per million characters. Each model publishes its own rate and its own minimum.
Read a render
GET/renders/{id}
Poll every few seconds. When status is succeeded:
{ "id": "c2737017ed54", "object": "render", "kind": "video", "model": "lightricks/ltx-2.5", "status": "succeeded", "created_at": "2026-09-12T15:04:05.000Z", "finished_at": "2026-09-12T15:04:41.000Z", "input": { "prompt": "…", "seconds": 4, "aspect": "16:9", "seed": 1837201 }, "output": { "url": "https://…r2.cloudflarestorage.com/…", "expires_at": "2026-09-13T15:04:41.000Z", "width": 1920, "height": 1088, "seconds": 4 }, "estimated_usd": 0.014, "usd": 0.014, "error": null}output.url is a signed link, valid 24 hours. Download the file and keep it: the link expires, the file does not. When status is failed, error.type says why and you were refunded.
List renders
GET/renders?limit=50&kind={family}&starting_after={id}
Your renders, newest first. 50 per page by default, 100 at most. Filter with kind (video, image, speech).
{ "object": "list", "data": [ { "id": "…", "object": "render", … } ], "has_more": false }To page, pass the last id you received as starting_after.
Webhooks
Give a webhook_url when you submit. When the render ends we POST the same object GET /renders/{id} would return, with two headers:
X-Klemet-Event: render.succeeded (or render.failed)X-Klemet-Signature: t=1789230000,v1=5f1a… (HMAC-SHA256)Verify the signature with the webhook signing secret of the key that submitted the render (whsec_…, shown at creation and revealable at klemet.app/dashboard/keys). The signed payload is `${t}.${rawBody}`:
curl
# Pseudo-shell: compute HMAC-SHA256 of "<t>.<raw body>" with your whsec_ secret# and compare it, constant-time, with v1. Reject if |now - t| > 300 s.Node
import { createHmac, timingSafeEqual } from "node:crypto"; export function verify(secret, rawBody, header) { const parts = Object.fromEntries(header.split(",").map((p) => p.split("="))); const t = Number(parts.t); if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false; const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));}Python
import hmac, hashlib, time def verify(secret: str, raw_body: bytes, header: str) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) t = int(parts["t"]) if abs(time.time() - t) > 300: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts["v1"])Answer 2xx quickly and do the work after. Without a 2xx, delivery is retried after 1, 5 and 25 minutes, then stops; GET always works as a fallback.
Idempotency
A connection can drop after we accepted your render but before you read our answer. To retry safely, send an Idempotency-Key header (any unique string up to 255 characters, and a UUID v4 is ideal):
curl -X POST https://klemet.app/api/v1/renders \ -H "Authorization: Bearer $KLEMET_KEY" \ -H "Idempotency-Key: 6f1c1e2a-3b4d-4c5e-9f00-1a2b3c4d5e6f" \ -H "content-type: application/json" \ -d '{"prompt":"…"}'For 24 hours, the same key with the same body returns the same render, and charges you once. The same key with a different body is refused with 409 idempotency_error.
Families
Pass a model id in model. These are the models the API renders today, and the price each one charges:
| Family | Model id | Price |
|---|---|---|
| Video | lightricks/ltx-2.5 | $0.004 /second |
| Video | wan-ai/wan2.2-ti2v-5b | $0.049 /second |
| Image | qwen/qwen-image-2512 | $0.029 /image |
| Image | black-forest-labs/flux.2-klein-4b | $0.003 /image |
| Speech | openbmb/voxcpm2 | $26.50 /M characters |
Leave model out and the render goes to lightricks/ltx-2.5. Any other id is refused with 400, so read GET /models rather than this page: it carries status and the current price for every model.
Errors
Every error is one object. type is for your code, message is for you, param names the field when one is at fault:
{ "error": { "type": "insufficient_funds", "message": "Your balance will not cover this render. Top up at klemet.app/dashboard/credits.", "param": null, "doc_url": "https://docs.klemet.app/#errors" }}| type | HTTP | Meaning |
|---|---|---|
| authentication_error | 401 | No key, an unknown key, or a revoked key. |
| invalid_request_error | 400 | A missing or malformed field. `param` names it. |
| insufficient_funds | 402 | Your balance cannot cover the estimate. `balance_usd` says what is left. |
| rate_limit_error | 429 | Too many requests. `Retry-After` says when to come back. |
| not_found | 404 | No render with this id on your account. |
| idempotency_error | 409 | This Idempotency-Key was already used with a different body. |
| engine_error | 502 | The engine refused or failed the render. You were not charged. |
| empty_output | 200 | The engine returned an empty clip (status `failed`). You were refunded; submit again. |
Rate limits
Per account: 20 requests per minute and 300 per hour. Beyond that, 429 with a Retry-After header in seconds. Ten cards render in parallel; a queue forms above that, which is normal: your renders are not lost, they wait.
Prices
GET /models returns the current price for every model, and each model page carries the same number.
Every render carries its own: estimated_usd when it is accepted, and usd when it lands. Both are what your balance is charged, and neither moves after the fact.
Versioning
This is version 2026-09-12. Send X-Klemet-Version: 2026-09-12 if you want to pin it; every response carries the version it was served with. When something changes in a way that could break you, a new date will be published here and the old one will keep working.