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).
| Method | Path | Result |
|---|---|---|
POST | /v1/studio/batches | Validate (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}/cancel | Cancel the rows not started yet |
POST | /v1/studio/batches/{id}/retry | Retry the failed rows |
GET | /v1/studio/batches/{id}/export.csv | Download the status of every row as CSV |
POST | /v1/studio/batches/{id}/zip | Build 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 withdelimiter. 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, booleanstrue/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 column | Also matches | Meaning |
|---|---|---|
title | titlu | Title of the row's creative (default: the source title) |
locale | limba, language, lang | Language of the row (BCP-47, e.g. en, de-DE) — see Languages |
format | aspect | Default 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.
| Field | Meaning |
|---|---|
source | { template_id } or { creative_id, seq? } (required) |
csv / sheet | The data, exactly one (required) |
delimiter | ,, ; or \t (default: detected) |
mapping | Column → variable key, title, locale, format or null |
title | Batch title, up to 200 characters |
render | true renders every row to MP4 (video sources only, otherwise 400 unsupported_kind; default false) |
quality | draft (default) or final |
format | Format id of the renders (default: the row's or the source's default format) |
localize | { "tone"?, "glossary"? } used for rows with a language |
dry_run | true 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.
| Limit | Value |
|---|---|
| Data rows | 500 |
| Columns | 50 |
| CSV size | 2 MB |
| Imported image | 10 MB, PNG / JPEG / WebP / GIF |
| Attempts per row | 3 |
| Files in the ZIP | 500 |
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 rowscodai::localize). - Each row render is billed like any render of the same quality, when it has succeeded.
cost_micro_usdsums 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.
Creatives
Editable creative documents in codai Studio — create a video, image or deck as JSON, edit it with ops, keep every version and render it to MP4.
Share, review and remix
Public read-only links to a creative, review links with anchored comments and approval, and remix of a shared creative into your own account.