Shared conventions
Pagination, idempotency, error shape and date handling — identical across every endpoint.
Response shape
An endpoint returning one resource returns that object directly, with no data wrapper:
{
"id": "0c2f…",
"title": "Morning bulletin",
"status": "RENDERING",
"targets": [ … ]
}An endpoint returning a list wraps it in the shared envelope, with the items under data:
{
"object": "list",
"url": "/api/v1/public/videos",
"data": [ … ],
"hasMore": true,
"totalCount": 137,
"page": 1,
"pageSize": 25
}JSON keys are camelCase, headers are kebab-case, error codes are snake_case. Every response carries an x-request-id header — quote it when reporting a problem and the logs are found in seconds.
Pagination
| Style | Parameters | Used by | Response |
|---|---|---|---|
| Page number | page, pageSize | videos, campaigns, sources, channels, templates | totalCount, page, pageSize, hasMore |
| Cursor | limit, startingAfter | media | hasMore — no total count |
pageSize defaults to 25 and is capped at 100. Media uses a cursor because files arrive constantly: with page numbers a file shifts between pages while you walk them, appearing twice or not at all.
Idempotency
Endpoints that spend money require an Idempotency-Key header: create video, queue a generation job, create a campaign, create a source, render and publish.
Idempotency-Key: 8f3b-morning-bulletin-2026-09-22
- You generate the string, 8–255 characters. One key per intent.
- Replaying the same key with the same body returns the original result, status code included — no second video, no second charge.
- The same key with a different body is rejected with
422. That means your code reused a key for another operation. - While the first call is still running, a second one gets
409. Wait a few seconds and retry with the same key — do not mint a new one. - Keys are scoped per endpoint and valid for 24 hours.
Lost the connection mid-call? Retry with the very same key
That is what the header exists for. Generating a fresh key for the retry creates a second video and a second charge.
Error format
Errors follow RFC 9457 with content-type: application/problem+json:
{
"type": "https://…/errors/insufficient-credits",
"title": "Unprocessable Entity",
"status": 422,
"detail": "Not enough credits for this operation",
"instance": "/api/v1/public/videos",
"code": "insufficient_credits",
"requestId": "req-01HXY7K3MN8P2RZ4QW9TB6FH3D",
"timestamp": "2026-09-22T02:00:00.000Z"
}requestId mirrors the x-request-id header — quote it when reporting a problem. Validation errors add an errors array where each entry names the offending path, its code and a message.
Branch on `code`, never on `detail`
detail is prose: it changes with the caller's language and can be reworded any time. code is the stable value.
| Status | Typically means |
|---|---|
| 400 / 422 | Missing or malformed field, or not enough credits |
| 401 | Missing or unusable key |
| 403 | Key lacks the scope for this path |
| 404 | Unknown id, or an id that belongs to another account |
| 409 | A call with the same idempotency key is still running |
| 429 | Calling too fast |
Dates and times
- Every timestamp returned is ISO 8601 in UTC, e.g.
2026-09-22T02:00:00.000Z. - Send ISO 8601 too, with an explicit offset so nothing depends on server locale.
- Sources and campaigns additionally carry a
timezonefield with an IANA name (e.g.Asia/Ho_Chi_Minh) because their schedules run in your local wall clock.
Rate limits
There is a cap on calls per time window. Going over returns 429; back off with increasing delays rather than retrying immediately.
Model providers impose their own limits on top. Firing dozens of video creations in parallel does not make anything faster — they queue at the render step, and bursts fail mid-way more often.
