Documents
codai Studio documents — build decks (PPTX), documents (DOCX) and spreadsheets (XLSX) from a JSON spec, preview them as PDF and PNG, and turn decks into narrated MP4s.
A document is an Office file built from a JSON spec: a deck (.pptx), a document (.docx) or a spreadsheet (.xlsx). You describe the content, the gateway builds the file synchronously and stores it as a Studio asset (kind: "document"). Files open and stay editable in PowerPoint, Word, Excel, LibreOffice, Google Slides/Docs/Sheets and Keynote.
| Method | Path | Result |
|---|---|---|
POST | /v1/studio/documents | 200 — the file is built; returns the document and its signed file.url |
GET | /v1/studio/documents/{id} | The document, a fresh file.url, the stored spec and latest_preview_id |
POST | /v1/studio/documents/{id}/previews | 202 — queues a preview (tagged PDF + page PNGs) |
GET | /v1/studio/previews/{id} | Preview status, and the PDF and pages when it has succeeded |
POST | /v1/studio/documents/{id}/video | 202 — turns a deck into an MP4 render |
Models and pricing
| Model | What | Price |
|---|---|---|
codai-docs | Building the PPTX / DOCX / XLSX file | free (cost_micro_usd: "0") |
codai-docs-preview | LibreOffice preview: tagged PDF, one PNG per page, contact sheet | $0.002 per page, minimum $0.01, billed once |
A preview is billed on the first poll that sees the result; failed previews cost nothing. Deck videos are billed as renders (see Deck → MP4).
Request
{
"model": "codai-docs",
"type": "deck",
"theme": "codai-light",
"title": "Rezultate T3",
"language": "ro-RO",
"filename": "rezultate-t3",
"preview": true,
"spec": {
"slides": [
{
"layout": "title",
"title": "Rezultate T3",
"subtitle": "Octombrie 2026",
"notes": "Bun venit."
},
{
"layout": "chart",
"title": "Venituri",
"chart": {
"chart_type": "column",
"alt": "Veniturile cresc de la 1,2 la 1,9 milioane lei",
"categories": ["Iul", "Aug", "Sep"],
"series": [{ "name": "Venituri", "values": [1.2, 1.5, 1.9] }]
}
}
]
}
}| Field | Default | Meaning |
|---|---|---|
type | — | deck (PPTX), doc (DOCX) or sheet (XLSX); required. Selects the schema of spec. |
spec | — | The content (required); must match type. See Spec reference. |
model | codai-docs | The only document model. |
theme | codai-light | codai-light, codai-dark or corporate (see below). |
title | first slide title / heading | Up to 200 characters; written to the file properties. |
language | ro-RO | BCP-47 proofing language of the text; also the default narration language. |
filename | — | Download name without extension (up to 120 characters, sanitised). |
based_on | — | One of your documents of the same type; creates the next version (see Editing). |
preview | false | Also queue a preview, billed like POST …/previews. |
The response:
{
"id": "5a7b2c3d-0000-4000-8000-000000000020",
"object": "studio.document",
"type": "deck",
"theme": "codai-light",
"model": "codai-docs",
"title": "Rezultate T3",
"version": 1,
"based_on": null,
"warnings": [],
"cost_micro_usd": "0",
"file": {
"asset_id": "5a7b2c3d-…",
"kind": "document",
"url": "https://storage.googleapis.com/…",
"mime_type": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"…": "…"
},
"preview": { "id": "6b8c3d4e-…", "object": "studio.preview", "status": "queued" }
}file.url is a signed URL valid for 1 hour; get a fresh one any time with GET /v1/studio/documents/{id} or GET /v1/assets/{id}. warnings lists non-fatal notes, such as a footnote that was ignored in a deck.
Themes
| Theme | Font | Look |
|---|---|---|
codai-light | Manrope | codai brand colours on a light background |
codai-dark | Manrope | codai brand colours on a dark background |
corporate | Aptos (Office substitutes Calibri when Aptos is missing) | neutral business style |
Fonts are not embedded in the files: a machine without Manrope shows a fallback font. Romanian diacritics (ș ț ă î â) are kept everywhere.
Spec reference
The full schemas are in the API reference (DocumentCreateRequest, DeckSpec, DocSpec, SheetSpec). Unknown fields are rejected.
Common types
- RichText — a string (up to 10,000 characters) or an array of 1–200 runs
{ text, bold?, italic?, underline?, code?, link?, footnote? }.linkmust be anhttpsURL;footnoteworks in DOCX only (ignored with a warning in decks). - Bullet — RichText, or
{ text, level }with an indent level of 0–3. - Image —
{ asset_id, alt, caption? }.asset_idis one of yourreadyimage assets (generated or uploaded);altis required. - Table —
{ columns (1–20), rows (≤ 500, cells are string | number | null), header? (true), align?, caption? }. A row may be shorter thancolumns, never longer. - Chart — see Charts.
Decks (type: "deck")
{ aspect? ("16:9" | "4:3", default 16:9), footer?, slide_numbers? (true), slides: Slide[1..60] }. Every slide picks a layout and may carry notes (speaker notes, up to 5,000 characters):
layout | Fields |
|---|---|
title | title, subtitle? |
section | title, subtitle? |
bullets | title, bullets (1–12) |
two_column | title, left, right — each column { heading?, text?, bullets?, image?, chart? } |
image | title?, image |
chart | title, chart |
table | title, table |
quote | quote, attribution? |
stats | title?, stats (1–4 of { value, label, delta? }) |
timeline | title, items (2–8 of { date, label, detail? }) |
closing | title, subtitle?, contact? |
Documents (type: "doc")
{ page_size? ("A4" | "Letter"), orientation? ("portrait" | "landscape"), header?, footer?, page_numbers? (true), toc? (false), blocks: Block[1..2000] }. toc: true inserts a table of contents built from the headings.
Block type | Fields |
|---|---|
heading | level (1–4), text |
paragraph | text (RichText), align? (left, center, right, justify) |
list | items (1–200 bullets), ordered? |
table | table |
image | image |
chart | chart |
quote | text, attribution? |
page_break | — |
Spreadsheets (type: "sheet")
{ sheets: Sheet[1..20] }, at most 200,000 cells in total. A sheet:
| Field | Default | Meaning |
|---|---|---|
name | — | Unique, 1–31 characters, none of []:*?/\. |
columns | — | { header, width?, format? } per column. |
rows | — | Arrays of cells. |
freeze_header | true | Freeze the header row. |
autofilter | true | Filter buttons on the header. |
totals | false | Add a SUM row for numeric columns. |
conditional | — | { range: "B2:B20", rule, value?, value2?, style? }; rules gt, lt, eq, between (need value, between also value2), color_scale, data_bar; style good, bad, neutral. |
charts | — | { chart, anchor?: "H2" }. |
A cell is a string, number, boolean, null, { "formula": "SUM(B2:B10)" } (no leading =) or { "date": "2026-10-10" } (ISO date or datetime). Column formats follow ro-RO conventions:
format | Excel format |
|---|---|
ron | #,##0.00 "lei" |
eur | #,##0.00 "€" |
usd | "$"#,##0.00 |
percent | 0.0% |
date | dd.mm.yyyy |
datetime | dd.mm.yyyy hh:mm |
text, integer, number | as named |
Charts
{ chart_type, alt, title?, categories?, series?, stacked?, number_format?, vega_lite? }, at most 20 charts per document.
chart_typeisbar,column,line,area,pie,doughnut,scatterorvega.- For every type except
vega,categoriesandseries(1–12 of{ name, values }) are required, and each series has exactly one value per category.number_formatis an Excel number format such as#,##0 "lei". vegatakes avega_litespec with inline data only — anyurl(data loaders, image marks) is rejected — up to 500 kB.
How they are drawn:
| PPTX | DOCX | XLSX | |
|---|---|---|---|
bar … scatter | native, editable chart | Vega-Lite PNG | Vega-Lite PNG + the data as a table next to it |
vega | Vega-Lite PNG | Vega-Lite PNG | Vega-Lite PNG + the data as a table next to it |
A chart that cannot be rendered fails the request with 400 chart_render_failed.
Accessibility
altis required on every image and chart; it becomes the alt text in the file and in the PDF.- Preview PDFs are tagged, and the reading order follows the order of the spec.
- Set
languageso screen readers and spell checkers use the right language.
Editing with based_on
GET /v1/studio/documents/{id} returns the stored spec. Change it and send it back with based_on set to the document id:
{ "type": "deck", "based_on": "5a7b2c3d-0000-4000-8000-000000000020", "spec": { "slides": [ … ] } }The result is a new document with version = previous + 1 and based_on pointing to the one you edited; the old file is left untouched. based_on must be one of your documents of the same type, otherwise 400 invalid_based_on.
Edit as a creative
Instead of editing the JSON spec, you can turn a document into an editable creative:
POST /v1/studio/documents/{id}/creative converts it (free, idempotent — the same creative comes back on later calls)
and links it to the document, which then reports creative_id. Edit it with ops or in the Studio editor with a live
preview, then POST /v1/studio/creatives/{id}/build builds a new version of this document (version + 1).
Previews
POST /v1/studio/documents/{id}/previews (or preview: true on create) converts the file with LibreOffice into:
- a tagged PDF (
pdf, an asset ofkind: "preview"); - one PNG per page, 1280 px wide, up to 60 pages (
pages); - a contact sheet with all pages on one image (
contact_sheet_url).
Poll GET /v1/studio/previews/{id} until status is succeeded or failed:
{
"id": "6b8c3d4e-0000-4000-8000-000000000021",
"object": "studio.preview",
"document_id": "5a7b2c3d-0000-4000-8000-000000000020",
"model": "codai-docs-preview",
"status": "succeeded",
"error": null,
"page_count": 2,
"cost_micro_usd": "10000",
"pdf": {
"asset_id": "6b8c3d4e-…",
"kind": "preview",
"mime_type": "application/pdf",
"url": "https://storage.googleapis.com/…",
"…": "…"
},
"pages": [
{ "page": 1, "url": "https://storage.googleapis.com/…p001.png…", "width": 1280, "height": 720 }
],
"contact_sheet_url": "https://storage.googleapis.com/…contact.png…"
}All URLs are valid for 1 hour. At most 3 previews can be queued or rendering per user at once (429 too_many_previews). The preview is what an agent should look at to check the layout before sending the file on.
Deck → MP4
POST /v1/studio/documents/{id}/video turns a deck into a video: the gateway queues a 1920×1080 preview, then a render (codai-render) that shows one slide per page, with a fade or no transition.
{
"quality": "final",
"seconds_per_slide": 5,
"transition": "fade",
"music_asset_id": "3f2b8c1e-0000-4000-8000-000000000002",
"narrate": { "model": "codai-speech", "voice": "Achernar", "language": "ro-RO" }
}| Field | Default | Meaning |
|---|---|---|
quality | final | final or draft, as for renders. |
seconds_per_slide | 5 | 2–20 s per slide when there is no narration. |
transition | fade | fade or none. |
music_asset_id | — | One of your ready audio assets, mixed under the slides at −18 dB. |
narrate | — | { model? ("codai-speech" | "codai-speech-expressive"), voice?, language?, instructions? }. |
With narrate, each slide's speaker notes are spoken (without notes: the title, subtitle and bullet text), and a slide lasts max(2 s, narration + 0.6 s). The whole video must fit in 180 seconds (400 video_too_long). Narration is not available for EU data-residency accounts (403 residency_unavailable).
The 202 response is a render (object: "studio.render") plus preview_id, narration_cost_micro_usd, slides and duration_seconds; poll it with GET /v1/studio/renders/{id}. Pricing:
- narration — billed immediately at the text-to-speech price (
narration_cost_micro_usd, also in thex-codai-narration-cost-micro-usdheader); - preview —
codai-docs-preview, $0.002 per slide page (minimum $0.01); - render —
codai-renderper output second ($0.01 final / $0.005 draft), billed on the render poll.
Limits
| Limit | Value |
|---|---|
| Slides per deck | 60 |
| Blocks per document | 2,000 |
| Sheets per spreadsheet | 20 |
| Cells per spreadsheet | 200,000 |
| Charts per document | 20 |
| Output file size | 50 MB |
| Preview pages rendered | 60 |
| Active previews per user | 3 |
| Deck video length | 180 s |
Errors
| HTTP | code | When |
|---|---|---|
| 400 | invalid_spec | The body does not match the schema; the first issues are in the message, the full list in details. |
| 400 | invalid_based_on | based_on is not one of your documents of the same type. |
| 400 | asset_not_found | An image (or the music asset) is unknown, not yours, not ready, or not an image (audio for music). |
| 400 | unsupported_image | The image format cannot be embedded in the file. |
| 400 | too_many_charts | More than 20 charts in one document. |
| 400 | chart_render_failed | A chart could not be rendered. |
| 400 | not_a_deck | Video requested for a doc or sheet. |
| 400 | video_too_long | The deck video would last more than 180 s. |
| 400 | bad_request | Invalid preview or video body. |
| 403 | forbidden | The key may not use codai-docs, codai-docs-preview, codai-render or the narration model. |
| 403 | residency_unavailable | Narration requested on an EU data-residency account. |
| 402 / 429 | subscription_inactive / rate_limit_exceeded | Inactive subscription or spend cap reached. |
| 404 | not_found | Unknown id, or another user's document or preview. |
| 413 | document_too_large | The generated file is larger than 50 MB. |
| 429 | too_many_previews | You already have 3 previews queued or rendering. |
| 429 | too_many_renders | You already have 3 renders queued or rendering. |
| 503 | documents_unavailable | Documents or previews are not configured on this gateway. |
Examples
curl
DOC=$(curl -s https://ai.codai.ro/v1/studio/documents \
-H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
-d @deck.json)
curl -L -o rezultate-t3.pptx "$(echo "$DOC" | jq -r .file.url)"
PREVIEW=$(echo "$DOC" | jq -r .preview.id)
while :; do
P=$(curl -s "https://ai.codai.ro/v1/studio/previews/$PREVIEW" -H "Authorization: Bearer $CODAI_API_KEY")
S=$(echo "$P" | jq -r .status)
[ "$S" = succeeded ] || [ "$S" = failed ] && break
sleep 3
done
curl -L -o rezultate-t3.pdf "$(echo "$P" | jq -r .pdf.url)"
# Deck -> narrated MP4, then poll /v1/studio/renders/{id}
curl -s "https://ai.codai.ro/v1/studio/documents/$(echo "$DOC" | jq -r .id)/video" \
-H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
-d '{"narrate":{"model":"codai-speech","language":"ro-RO"}}'TypeScript SDK
import { Codai } from 'codai-sdk';
const client = new Codai({ apiKey: process.env.CODAI_API_KEY! });
const doc = await client.documents.create({
type: 'sheet',
theme: 'corporate',
filename: 'facturi-octombrie',
spec: {
sheets: [
{
name: 'Facturi',
columns: [
{ header: 'Client' },
{ header: 'Data', format: 'date' },
{ header: 'Total', format: 'ron' },
],
rows: [
['Alfa SRL', { date: '2026-10-01' }, 1250.5],
['Beta SA', { date: '2026-10-03' }, 980],
],
totals: true,
},
],
},
});
console.log(doc.file.url);
const queued = await client.documents.preview(doc.id);
const preview = await client.documents.retrievePreview(queued.id);
const again = await client.documents.retrieve(doc.id); // includes the stored specFor decks, client.documents.video(id, { narrate: { model: 'codai-speech' } }) returns the queued render; poll it with client.renders.retrieve(render.id).
Python SDK
import os
from codai import Codai
client = Codai(api_key=os.environ["CODAI_API_KEY"])
doc = client.documents.create({
"type": "doc",
"title": "Raport lunar",
"spec": {
"toc": True,
"blocks": [
{"type": "heading", "level": 1, "text": "Raport lunar"},
{"type": "paragraph", "text": [{"text": "Veniturile au crescut cu "}, {"text": "18%", "bold": True}, {"text": "."}]},
{"type": "table", "table": {"columns": ["Lună", "Venituri"], "rows": [["Septembrie", 1.5], ["Octombrie", 1.9]]}},
],
},
})
print(doc["file"]["url"])
queued = client.documents.preview(doc["id"])
preview = client.documents.retrieve_preview(queued["id"])
stored = client.documents.retrieve(doc["id"]) # includes the stored specclient.documents.video(deck_id, {...}) queues a deck video; poll it with client.renders.retrieve(render["id"]).