codai docs
Gateway

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.

MethodPathResult
POST/v1/studio/documents200 — 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}/previews202 — 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}/video202 — turns a deck into an MP4 render

Models and pricing

ModelWhatPrice
codai-docsBuilding the PPTX / DOCX / XLSX filefree (cost_micro_usd: "0")
codai-docs-previewLibreOffice 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] }]
        }
      }
    ]
  }
}
FieldDefaultMeaning
type—deck (PPTX), doc (DOCX) or sheet (XLSX); required. Selects the schema of spec.
spec—The content (required); must match type. See Spec reference.
modelcodai-docsThe only document model.
themecodai-lightcodai-light, codai-dark or corporate (see below).
titlefirst slide title / headingUp to 200 characters; written to the file properties.
languagero-ROBCP-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).
previewfalseAlso 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

ThemeFontLook
codai-lightManropecodai brand colours on a light background
codai-darkManropecodai brand colours on a dark background
corporateAptos (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? }. link must be an https URL; footnote works 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_id is one of your ready image assets (generated or uploaded); alt is required.
  • Table — { columns (1–20), rows (≤ 500, cells are string | number | null), header? (true), align?, caption? }. A row may be shorter than columns, 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):

layoutFields
titletitle, subtitle?
sectiontitle, subtitle?
bulletstitle, bullets (1–12)
two_columntitle, left, right — each column { heading?, text?, bullets?, image?, chart? }
imagetitle?, image
charttitle, chart
tabletitle, table
quotequote, attribution?
statstitle?, stats (1–4 of { value, label, delta? })
timelinetitle, items (2–8 of { date, label, detail? })
closingtitle, 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 typeFields
headinglevel (1–4), text
paragraphtext (RichText), align? (left, center, right, justify)
listitems (1–200 bullets), ordered?
tabletable
imageimage
chartchart
quotetext, attribution?
page_break—

Spreadsheets (type: "sheet")

{ sheets: Sheet[1..20] }, at most 200,000 cells in total. A sheet:

FieldDefaultMeaning
name—Unique, 1–31 characters, none of []:*?/\.
columns—{ header, width?, format? } per column.
rows—Arrays of cells.
freeze_headertrueFreeze the header row.
autofiltertrueFilter buttons on the header.
totalsfalseAdd 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:

formatExcel format
ron#,##0.00 "lei"
eur#,##0.00 "€"
usd"$"#,##0.00
percent0.0%
datedd.mm.yyyy
datetimedd.mm.yyyy hh:mm
text, integer, numberas named

Charts

{ chart_type, alt, title?, categories?, series?, stacked?, number_format?, vega_lite? }, at most 20 charts per document.

  • chart_type is bar, column, line, area, pie, doughnut, scatter or vega.
  • For every type except vega, categories and series (1–12 of { name, values }) are required, and each series has exactly one value per category. number_format is an Excel number format such as #,##0 "lei".
  • vega takes a vega_lite spec with inline data only — any url (data loaders, image marks) is rejected — up to 500 kB.

How they are drawn:

PPTXDOCXXLSX
bar … scatternative, editable chartVega-Lite PNGVega-Lite PNG + the data as a table next to it
vegaVega-Lite PNGVega-Lite PNGVega-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

  • alt is 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 language so 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 of kind: "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" }
}
FieldDefaultMeaning
qualityfinalfinal or draft, as for renders.
seconds_per_slide52–20 s per slide when there is no narration.
transitionfadefade 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 the x-codai-narration-cost-micro-usd header);
  • preview — codai-docs-preview, $0.002 per slide page (minimum $0.01);
  • render — codai-render per output second ($0.01 final / $0.005 draft), billed on the render poll.

Limits

LimitValue
Slides per deck60
Blocks per document2,000
Sheets per spreadsheet20
Cells per spreadsheet200,000
Charts per document20
Output file size50 MB
Preview pages rendered60
Active previews per user3
Deck video length180 s

Errors

HTTPcodeWhen
400invalid_specThe body does not match the schema; the first issues are in the message, the full list in details.
400invalid_based_onbased_on is not one of your documents of the same type.
400asset_not_foundAn image (or the music asset) is unknown, not yours, not ready, or not an image (audio for music).
400unsupported_imageThe image format cannot be embedded in the file.
400too_many_chartsMore than 20 charts in one document.
400chart_render_failedA chart could not be rendered.
400not_a_deckVideo requested for a doc or sheet.
400video_too_longThe deck video would last more than 180 s.
400bad_requestInvalid preview or video body.
403forbiddenThe key may not use codai-docs, codai-docs-preview, codai-render or the narration model.
403residency_unavailableNarration requested on an EU data-residency account.
402 / 429subscription_inactive / rate_limit_exceededInactive subscription or spend cap reached.
404not_foundUnknown id, or another user's document or preview.
413document_too_largeThe generated file is larger than 50 MB.
429too_many_previewsYou already have 3 previews queued or rendering.
429too_many_rendersYou already have 3 renders queued or rendering.
503documents_unavailableDocuments 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 spec

For 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 spec

client.documents.video(deck_id, {...}) queues a deck video; poll it with client.renders.retrieve(render["id"]).

On this page