codai docs
Gateway

Batches

Fill a template or a creative from a CSV file or a sheet — one creative per row, with optional per-row language and render, status export and a ZIP of the videos.

A batch turns a table into creatives: each row of a CSV file (or of a sheet creative) fills the variables of a template or of a creative and becomes a new creative of its own. Rows can be translated into their own language and rendered to MP4. Batches run in the background; you follow them by polling.

Creating a batch is free. Translations and renders made for the rows are billed exactly like the same calls made by hand (see Billing).

MethodPathResult
POST/v1/studio/batchesValidate (dry_run) or create a batch (201)
GET/v1/studio/batches?limit=&before={ batches, next_before }, newest first
GET/v1/studio/batches/{id}?rows=&status=&offset=&limit=The batch, optionally with its rows
POST/v1/studio/batches/{id}/cancelCancel the rows not started yet
POST/v1/studio/batches/{id}/retryRetry the failed rows
GET/v1/studio/batches/{id}/export.csvDownload the status of every row as CSV
POST/v1/studio/batches/{id}/zipBuild a ZIP of the finished renders (202)

Sources and data

The source is exactly one of { "template_id": "<id>" } (a template with at least one variable) or { "creative_id": "<id>", "seq"?: <version> } (a creative with at least one unlocked variable; default the head). Otherwise the answer is 400 no_vars.

The data is exactly one of:

  • csv — CSV text with a header row, up to 2 MB. The parser follows RFC 4180: quoted fields, "" inside quotes, line breaks inside quoted cells, CRLF / LF / CR line endings; a UTF-8 BOM is removed. The delimiter is detected from the header (,, ; or tab — a spreadsheet exported with a Romanian locale usually uses ;) or forced with delimiter. Header cells are trimmed, short rows are padded with empty cells and empty trailing rows are dropped.
  • sheet — { "creative_id": "<sheet creative>", "node_id"?: "<grid id>" }: the first (or given) grid or table of a sheet, doc or deck creative. The header is the column headers when every column has one, otherwise the first row; dates become ISO dates, booleans true / false, numbers plain text and formula cells are left empty.

Limits. At most 500 data rows and 50 columns. CSV errors answer 400 with the code: empty, ragged (a row longer than the header, with its index), unterminated_quote, too_many_columns, too_many_rows.

Mapping columns

Each column maps to a variable, to a reserved column or to nothing. Without a mapping the columns are matched automatically: diacritics are removed, letters lowercased and every other character run becomes _, then the column name is compared with each variable key and label (Titlu principal matches the variable titlu_principal).

Reserved columnAlso matchesMeaning
titletitluTitle of the row's creative (default: the source title)
localelimba, language, langLanguage of the row (BCP-47, e.g. en, de-DE) — see Languages
formataspectDefault format of the row's creative (a format id of the source)

mapping overrides the automatic match per column: { "Headline EN": "headline", "Notes": null, "Lang": "locale" } (null ignores the column). A target that is neither a variable nor a reserved column answers 400 invalid_mapping.

Cell values. An empty cell keeps the variable's default. Numbers accept 1.5, 1,5 and 1 234,5; booleans true / false, yes / no, da / nu, 1 / 0 and x; colours #rgb, #rrggbb and #rrggbbaa; texts up to 5000 characters.

Image cells. An image variable takes one of:

  • an asset id of one of your ready images;
  • an asset name — the newest of your ready images whose name or file name matches (case-insensitive);
  • an https URL — imported into your Studio library when the row runs (public addresses only, every redirect re-checked; PNG, JPEG, WebP or GIF up to 10 MB). The same URL is imported once per batch.

Dry run

Send "dry_run": true to check everything without storing anything (free):

curl https://ai.codai.ro/v1/studio/batches \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "source": { "template_id": "<template id>" }, "csv": "headline,photo,locale\nAutumn sale,https://example.com/a.jpg,en\nReduceri de toamnă,poza-1,ro\n", "dry_run": true }'
{
  "dry_run": true,
  "columns": [{ "name": "headline", "index": 0, "maps_to": "headline", "kind": "var" }],
  "unmapped_vars": [],
  "rows": 2,
  "valid": 2,
  "errors": [],
  "preview": [{ "row": 0, "title": "Promo 9:16", "locale": "en", "format": null, "vars": { "headline": "Autumn sale" } }],
  "estimate": { "renders": 0, "localized_rows": 2 }
}

errors lists up to 100 problems as { "row", "column", "message" } (an invalid number, an unknown image, a bad language, a format the source does not have); preview shows the first three valid rows; estimate counts the renders and translations the batch would bill. Fix the mapping and try again until it looks right.

Create and follow a batch

The same body without dry_run creates the batch and answers 201 { "batch": { … } }. Invalid rows are stored as failed with their error and the rest run; when every row is invalid the answer is 400 invalid_rows with the errors.

FieldMeaning
source{ template_id } or { creative_id, seq? } (required)
csv / sheetThe data, exactly one (required)
delimiter,, ; or \t (default: detected)
mappingColumn → variable key, title, locale, format or null
titleBatch title, up to 200 characters
rendertrue renders every row to MP4 (video sources only, otherwise 400 unsupported_kind; default false)
qualitydraft (default) or final
formatFormat id of the renders (default: the row's or the source's default format)
localize{ "tone"?, "glossary"? } used for rows with a language
dry_runtrue validates only

Each row goes through pending → created → (localized) → (rendering) → done, or ends as failed or skipped. GET /v1/studio/batches/{id} advances the batch a little on every poll and answers { "batch": { "id", "title", "status", "source", "total", "done", "failed", "pending", "skipped", "options", "columns", "cost_micro_usd", "zip", "created_at", "updated_at", "finished_at" } }; with rows=1 it adds rows (offset, limit up to 100, optional status filter), each { "idx", "status", "creative_id", "render_id", "render_url", "locale", "title", "error", "cost_micro_usd" } (render_url is a signed link once the render is ready). columns is { "header", "columns", "unmapped_vars" }: the CSV header, the same column mapping as the dry run and the variables no column fills. cost_micro_usd is a decimal string (USD micros). The batch status is queued, running, completed, failed or cancelled. A batch that is not yours answers 404 Batch not found..

Cancel and retry. POST …/cancel marks the rows not started yet as skipped and the batch cancelled (409 not_cancellable once it is finished). POST …/retry puts the failed rows back in the queue — each row is tried at most 3 times (409 nothing_to_retry when no row can be retried). Both answer with the batch.

Languages

With a locale column, each row's creative is first translated into that language with the model (the same translation as Localisation, with the batch's localize.tone and localize.glossary) and then becomes a creative in that language, ready to render. Leave the cell empty to keep the source language.

Export, ZIP and limits

GET …/export.csv downloads the original columns plus creative_id, status, error and render_url for every row (text/csv; charset=utf-8, <title>-status.csv). Cells starting with =, +, -, @, a tab or a carriage return get a leading ', so a spreadsheet never runs them as formulas. Free.

POST …/zip builds a ZIP of every finished render (<idx>-<title>.mp4, at most 500 files) in the background and answers 202 with the batch (zip.status: "building"); poll the batch until zip.status is ready and download zip.url (signed for 1 hour) — or failed with zip.error. No finished render answers 409 no_renders. Free.

LimitValue
Data rows500
Columns50
CSV size2 MB
Imported image10 MB, PNG / JPEG / WebP / GIF
Attempts per row3
Files in the ZIP500

Billing

  • Creating, validating, polling, cancelling, retrying, exporting and building the ZIP are free.
  • Each row with a language is translated and billed like POST …/localize (usage rows codai::localize).
  • Each row render is billed like any render of the same quality, when it has succeeded.
  • cost_micro_usd sums what the batch and each row cost so far.

SDKs and agent tools

const check = await codai.batches.dryRun({ source: { template_id: tpl.id }, csv });
if (check.valid === check.rows) {
  const { batch } = await codai.batches.create({ source: { template_id: tpl.id }, csv, render: true });
  await codai.batches.wait(batch.id); // polls every 3 s, up to 10 min
  const statusCsv = await codai.batches.exportCsv(batch.id);
  await codai.batches.zip(batch.id);
}
check = client.batches.dry_run(source={"template_id": tpl["id"]}, csv=csv_text)
batch = client.batches.create(source={"template_id": tpl["id"]}, csv=csv_text, render=True)["batch"]
client.batches.wait(batch["id"])  # polls every 3 s, up to 10 min
status_csv = client.batches.export_csv(batch["id"])

Agents on desktop, phone and VS Code use creative_batch (template_id or creative_id, csv or sheet_creative_id, mapping, render, quality, format, dry_run) and creative_batch_status (batch_id, rows).

Hub (internal)

The hub calls the same routes under /internal/studio/batches… with a service token and a user_id; bodies and errors are identical.

On this page