API v1
The Clipelle API
Everything the form can do, from your own code: create an ad from a product and image links, translate a video you own, and read the result. A key acts as your account: the same credits, the same cost limits, the same projects on your dashboard. Keys are made on Settings → API keys and come with a plan that includes credits.
Authentication
Send the key as a bearer token on every request. Keys start with rl_, are shown once when created, and can be revoked at any time. Requests without a valid key get 401 unauthorized; a key on an account without a plan gets 403 plan_required.
Authorization: Bearer rl_your_keyCreate an ad
POST /api/v1/ads creates the ad and starts it. The body takes the same fields as the form; image links must be public (PNG, JPG or WebP, up to 10 MB each, at most 5). The response is 201 with the project; poll it until status is completed.
curl -X POST https://YOUR-DOMAIN/api/v1/ads \
-H "Authorization: Bearer rl_your_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234" \
-d '{
"name": "Nimbus bottle, spring ad",
"productName": "Nimbus 750 ml insulated bottle",
"description": "Double-walled steel bottle that keeps drinks cold for 24 hours and hot for 12. Fits a car cup holder.",
"productUrl": "https://yourshop.com/products/nimbus-750",
"images": [{ "url": "https://yourshop.com/images/nimbus-front.jpg" }],
"seconds": 30,
"quality": "standard",
"language": "en",
"tone": "direct",
"offer": "Free shipping this week"
}'const res = await fetch("https://YOUR-DOMAIN/api/v1/ads", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CLIPELLE_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "order-1234",
},
body: JSON.stringify({
name: "Nimbus bottle, spring ad",
productName: "Nimbus 750 ml insulated bottle",
description: "Double-walled steel bottle that keeps drinks cold for 24 hours and hot for 12.",
images: [{ url: "https://yourshop.com/images/nimbus-front.jpg" }],
seconds: 30,
quality: "standard",
language: "en",
}),
});
const { project, error } = await res.json();
if (!res.ok) throw new Error(`${error.code}: ${error.message}`);
console.log(project.id, project.status); // "processing"Fields
name(2–120),productName(1–120),description(20–4000 characters; the script is written from it).images: 1–5 objects with a publicurl. Optionallogoandbackgroundobjects with aurl.seconds: one of 10, 20, 30, 45, 60 (default 30).quality:standard(default) orpremium.language,accent,voiceId,lookId,speed(slower,normal,faster): the same options the form offers; leave them out for the defaults.tone(elegant,playful,direct,warm),offer(80),direction(200),script(your own text instead of a written one),productUrl,backgroundColor(hex),brandwithaccent,captionStyle,font.format: a preset from the format library (unboxing,testimonial,before-after,problem-fix,three-reasons,product-demo,founder-story,offer). It is stored on the project; send the matchingdirectionandtoneyourself (the app form does that for you), the API does not rewrite your fields.
Script review is a form-only feature: an API project is made in one go.
{
"project": {
"id": "7c0d…",
"kind": "ad",
"name": "Nimbus bottle, spring ad",
"status": "processing",
"createdAt": "2026-10-21T10:00:00.000Z",
"steps": [
{ "key": "script", "label": "AI script", "status": "processing" },
{ "key": "productClip", "label": "AI product footage", "status": "waiting" },
{ "key": "presenter", "label": "AI presenter", "status": "waiting" },
{ "key": "finalEdit", "label": "Captions and 9:16", "status": "waiting" }
],
"result": {},
"ad": { "seconds": 30, "quality": "standard", "language": "en" }
}
}Translate a video
POST /api/v1/translations downloads the video from a public link (MP4, WebM or QuickTime, up to 32 MB and 180 seconds) and translates it into every language in targetLanguages. rightsConfirmed must be true: by sending it you confirm you own the video and have the speaker's permission to use their voice and face. video.durationSeconds is the length of the file; the estimate and the credits follow from it.
curl -X POST https://YOUR-DOMAIN/api/v1/translations \
-H "Authorization: Bearer rl_your_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Explainer, German and French",
"video": { "url": "https://yourcdn.com/explainer.mp4", "durationSeconds": 42 },
"sourceLanguage": "auto",
"targetLanguages": ["de", "fr"],
"quality": "speed",
"format": "9:16",
"enhance": false,
"rightsConfirmed": true
}'Options: quality (speed or precision), format (9:16 or original), enhance, keepTerms (up to 20 words to keep as they are), trimSilence, captionPosition (low or middle). captionStyle (all optional: show true/false, highlight box | color | underline | none, color one of #D43F1B, #FFD400, #22C55E, #2563EB, #EC4899, #FFFFFF, size small | medium | large, uppercase true/false), removeMusic and cleanVoice (true/false). replaceTerms (up to 10 pairs { from, to }: say to wherever the video says from), instructions (wishes for the wording, up to 300 characters), and in captionStyle a bar (none | white | cream | yellow | dark) behind the captions. Language codes are the ones the form offers.
Read projects
GET /api/v1/projects?limit=50 lists your projects, newest first (limit 1–100). GET /api/v1/projects/{id} returns one. Someone else's project is a 404, never a 403. Result links are signed and valid for about an hour (result.expiresInSeconds); fetch the project again for a fresh link.
async function waitForVideo(id) {
for (;;) {
const res = await fetch(`https://YOUR-DOMAIN/api/v1/projects/${id}`, {
headers: { Authorization: `Bearer ${process.env.CLIPELLE_API_KEY}` },
});
const { project } = await res.json();
if (project.status === "completed") return project.result.videoUrl; // valid for about an hour
if (project.status === "failed") throw new Error(project.steps.find((s) => s.error)?.error ?? "failed");
await new Promise((r) => setTimeout(r, 15_000));
}
}Statuses: processing, completed, failed (a step carries a plain-language error), needs_input (a form project waiting for a review), draft. Step keys are generic: script, productClip, presenter, finalEdit for ads; upload, translate, captions for translations, with one entry per language under translations.
Idempotency
Send an Idempotency-Key header (up to 200 characters, unique per request you mean) on the two create calls. A retry with the same key and the same body returns the project made the first time with 200 instead of making a second video. The same key with a different body is refused with 409 idempotency_mismatch. Keys are scoped to the API key that sent them.
Errors
Every error is JSON with a stable code and a message you can show to a person.
{ "error": { "code": "insufficient_credits", "message": "You need 30 credits for this video and have 12. Add a credit pack or wait for your next month." } }| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | A field is missing or out of range; details lists the paths. |
| 401 | unauthorized | No key, an unknown key, or a revoked key. |
| 402 | insufficient_credits | The project needs more credits than the account has. |
| 403 | plan_required | The account has no plan with credits (or only the free plan). |
| 404 | not_found | No such project for this account. |
| 409 | idempotency_mismatch | The Idempotency-Key was used with a different body. |
| 409 | budget_exceeded | The estimate is above the per-video budget, or a spend limit was reached. |
| 413 / 415 | file_too_large / file_content_type | A linked file is too large or not an image/video. |
| 429 | rate_limited | Too many requests for this key; Retry-After says when. |
| 500 | server_error | Something failed on our side; retry with the same Idempotency-Key. |
| 503 | unavailable | The API is off while the server runs in demo mode. |
Webhooks
Instead of polling, let us call you. In Settings → API keys → Webhooks add a public https:// URL and pick the events project.completed and project.failed. The signing secret is shown once.
{
"id": "7a1c…", // delivery id, also in X-Clipelle-Delivery
"event": "project.completed", // or project.failed
"createdAt": "2026-10-21T14:02:11.000Z",
"data": { ...the project exactly as GET /api/v1/projects/{id} returns it }
}Every call carries X-Clipelle-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "t." + raw body with your secret, plus X-Clipelle-Event and X-Clipelle-Delivery. Check it like this:
import { createHmac, timingSafeEqual } from "node:crypto";
// Express-style handler: read the RAW body (not re-serialised JSON).
export function verifyClipelle(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false; // 5-minute window
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return parts.v1?.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
app.post("/clipelle/webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyClipelle(req.body.toString("utf8"), req.get("X-Clipelle-Signature") ?? "", process.env.CLIPELLE_WEBHOOK_SECRET)) return res.sendStatus(401);
const { event, data } = JSON.parse(req.body.toString("utf8"));
// Answer fast; do the work afterwards. The same delivery can arrive more than once: dedupe on its id.
res.sendStatus(200);
});- Answer with any 2xx within 10 seconds. Anything else is retried with backoff, five more times over about a day; after that the delivery is marked failed and can be sent again from Settings.
- A delivery can arrive more than once (a slow answer counts as a failure). Deduplicate on
id. - Redirects are not followed; private addresses and plain http are refused when you add the URL.
Limits
- 60 requests per minute per key. Creating a video also counts towards your account's daily and monthly spend limits.
- Linked files: public http(s) links only; private or internal addresses are refused; at most 3 redirects; images 10 MB, logos 2 MB, videos 32 MB.
- Credits and costs are exactly those of the form; the project page in Studio shows the real amount afterwards.
- No webhooks yet: poll the project. Result links expire after about an hour.
Want an assistant to call the API for you? See the MCP server.
