# Documents (/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](./assets.mdx) (`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 [#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](./renders.mdx) (see [Deck → MP4](#deck--mp4)).

## Request [#request]

```json
{
  "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](#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](#editing-with-based_on)). |
| `preview`  | `false`                     | Also queue a preview, billed like `POST …/previews`.                                                        |

The response:

```json
{
  "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 [#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 [#spec-reference]

The full schemas are in the [API reference](../api/index.mdx) (`DocumentCreateRequest`, `DeckSpec`, `DocSpec`, `SheetSpec`). Unknown fields are rejected.

### Common types [#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](#charts).

### Decks (`type: "deck"`) [#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"`) [#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"`) [#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 [#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:

|                   | 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 [#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` [#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:

```json
{ "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 [#edit-as-a-creative]

Instead of editing the JSON `spec`, you can turn a document into an editable [creative](./creatives.mdx#documents-deck-doc-sheet):
`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 [#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`:

```json
{
  "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 [#deck--mp4]

`POST /v1/studio/documents/{id}/video` turns a **deck** into a video: the gateway queues a 1920×1080 preview, then a [render](./renders.mdx) (`codai-render`) that shows one slide per page, with a fade or no transition.

```json
{
  "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](./embeddings-audio.mdx) 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 [#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 [#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 [#examples]

### curl [#curl]

```bash
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 [#typescript-sdk]

```ts
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 [#python-sdk]

```python
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"])`.
