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.

Headerhttp
Authorization: Bearer rl_your_key

Create 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.

curlbash
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"
  }'
JavaScript (Node 18+)javascript
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 public url. Optional logo and background objects with a url.
  • seconds: one of 10, 20, 30, 45, 60 (default 30). quality: standard (default) or premium.
  • 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), brand with accent, 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 matching direction and tone yourself (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.

Responsejson
{
  "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.

curlbash
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.

Polling until the video is readyjavascript
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 shapejson
{ "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." } }
StatusCodeMeaning
400validation_errorA field is missing or out of range; details lists the paths.
401unauthorizedNo key, an unknown key, or a revoked key.
402insufficient_creditsThe project needs more credits than the account has.
403plan_requiredThe account has no plan with credits (or only the free plan).
404not_foundNo such project for this account.
409idempotency_mismatchThe Idempotency-Key was used with a different body.
409budget_exceededThe estimate is above the per-video budget, or a spend limit was reached.
413 / 415file_too_large / file_content_typeA linked file is too large or not an image/video.
429rate_limitedToo many requests for this key; Retry-After says when.
500server_errorSomething failed on our side; retry with the same Idempotency-Key.
503unavailableThe 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.

Payloadjson
{
  "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:

Verifying the signature (Node)javascript
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.