Errors and limits
The one error shape, what to do on each status, and how to handle a 429
Every error from this API, on every route and every status, has the same body. Learn to read one and you have read them all.
{
"code": "INSUFFICIENT_CREDITS",
"extra": { "credits_needed": 121, "current_balance": 40 }
}codeis a stable, uppercase identifier from a closed set. This is what yourswitchdecides on.extraonly shows up when there is something to act on: what was missing, which field failed, which ids were rejected.- There is no ready-made message. The server does not know the language of the person using your product, and prose cannot be handled in code. You write the sentence.
States are lowercase, errors are uppercase. status says where something is (queued, processing, completed, failed); code and error_code say what went wrong (VIDEO_TOO_LONG, DOWNLOAD_NO_AUDIO).
What to do on each status
| Status | What it means | What to do |
|---|---|---|
400 | The request body is wrong | Fix the payload. Sending it again unchanged will not help |
401 | Key missing, revoked or expired | Check the X-Api-Key header |
402 | Not enough credits | Top up, or read extra.credits_needed |
403 | The video or the account is off limits | Nothing to retry |
404 | Does not exist in this workspace | Check the id and the workspace |
409 | It exists, but its state refuses the operation | Wait and try again |
410 | It existed and expired | Submit or render again |
422 | Valid link, unusable video | Try another source |
429 | Rate limited | Wait for Retry-After |
5xx | Our fault | Retry with backoff |
The full list of code values per route is in the reference, response by response.
Handling errors in your code
The pattern that covers everything: read the code, let the status decide whether it is worth retrying.
const RETRIABLE = new Set([429, 500, 502, 503, 504]);
async function call(path, init = {}, attempt = 1) {
const response = await fetch(`https://api.cut.pro/api/v1${path}`, {
...init,
headers: { "X-Api-Key": process.env.CUTPRO_API_KEY, "Content-Type": "application/json" },
});
if (response.ok) return response.json();
const body = await response.json();
if (RETRIABLE.has(response.status) && attempt <= 5) {
// The server says how long to wait on a 429. On 5xx, exponential backoff.
const retryAfter = Number(response.headers.get("retry-after"));
const wait = retryAfter > 0 ? retryAfter : 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
return call(path, init, attempt + 1);
}
throw Object.assign(new Error(body.code), { code: body.code, extra: body.extra, status: response.status });
}Then your product turns the code into the language of whoever is looking:
switch (error.code) {
case "INSUFFICIENT_CREDITS":
show(`You need ${error.extra.credits_needed - error.extra.current_balance} more credits.`);
break;
case "VIDEO_TOO_LONG":
show(`That video is ${error.extra.duration}s and your plan takes up to ${error.extra.max_duration}s.`);
break;
case "LIVE_STREAM":
show("The stream is still on air. Try again once it becomes a video.");
break;
default:
show("We could not process that right now. Try again in a moment.");
}Rate limits
600 requests per minute, per API key, in a fixed 60-second window. Every response tells you where you stand:
| Header | What it carries |
|---|---|
X-RateLimit-Limit | The ceiling (600) |
X-RateLimit-Remaining | Calls still available in this window |
X-RateLimit-Reset | Unix seconds for when the window resets |
Over the ceiling you get 429 with { "code": "RATE_LIMIT_EXCEEDED" } and a Retry-After header in seconds. Honour it: retrying immediately only burns the next window.
POST /clips/info has its own, tighter limit of 10 per minute, because every call reaches out to the source platform for metadata. Analyze once and keep the id.
The limit is per key, not per IP: several of your processes sharing a key share the same budget, and two keys behind the same IP never get in each other's way.
Does polling fit in that?
Comfortably. Polling one submission every 10 seconds costs 6 requests per minute, so you can keep dozens of jobs in flight. What blows the limit is a tight loop with no wait.