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

StyleParametersUsed byResponse
Page numberpage, pageSizevideos, campaigns, sources, channels, templatestotalCount, page, pageSize, hasMore
Cursorlimit, startingAftermediahasMore — 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.

StatusTypically means
400 / 422Missing or malformed field, or not enough credits
401Missing or unusable key
403Key lacks the scope for this path
404Unknown id, or an id that belongs to another account
409A call with the same idempotency key is still running
429Calling 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 timezone field 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.