---
title: Compositions and Studio export
description: "HTML timeline compositions: store them, render an MP4 or a still on the server, and export for Premiere Pro, DaVinci Resolve or After Effects."
---

# Compositions and Studio export

A composition is an HTML timeline stored as a file bundle: `index.html`, optional sub-compositions, media files and metadata. Studio edits live in `nolgia-edits.json`. Rendering and editor export both use the effective timeline: authored timing plus that edit overlay.

<!-- gen:endpoints tag=Compositions -->
| Method | Path | What it does |
| --- | --- | --- |
| [GET](../api/#tag/compositions/get/compositions) | `/compositions` | List the current user's compositions, newest first. |
| [POST](../api/#tag/compositions/post/compositions) | `/compositions` | Create an empty composition. |
| [GET](../api/#tag/compositions/get/compositions/{id}) | `/compositions/{id}` | Fetch one of the current user's compositions, including its file inventory. |
| [PATCH](../api/#tag/compositions/patch/compositions/{id}) | `/compositions/{id}` | Update a composition's name, description, project link, or meta. |
| [DELETE](../api/#tag/compositions/delete/compositions/{id}) | `/compositions/{id}` | Delete a composition and all of its files. |
| [PUT](../api/#tag/compositions/put/compositions/{id}/file) | `/compositions/{id}/file` | Upload or overwrite one file in a composition. |
| [DELETE](../api/#tag/compositions/delete/compositions/{id}/file) | `/compositions/{id}/file` | Delete one file from a composition. |
| [POST](../api/#tag/compositions/post/compositions/{id}/files:signed-urls) | `/compositions/{id}/files:signed-urls` | Mint fresh signed GET URLs for composition files and referenced platform assets. |
| [POST](../api/#tag/compositions/post/compositions/{id}/clone) | `/compositions/{id}/clone` | Clone a composition, copying its full file bundle. |
| [POST](../api/#tag/compositions/post/compositions/{id}/imports/figma) | `/compositions/{id}/imports/figma` | Import a Figma frame into a composition as text layers and PNG stills. |
| [POST](../api/#tag/compositions/post/compositions/{id}/render) | `/compositions/{id}/render` | Queue a server-side MP4 or single-frame PNG export of the composition's effective timeline. |
| [GET](../api/#tag/compositions/get/compositions/{id}/renders) | `/compositions/{id}/renders` | List the composition's renders, newest first (at most 20). |
| [GET](../api/#tag/compositions/get/compositions/{id}/export) | `/compositions/{id}/export` | Export the composition for editing in Premiere Pro, DaVinci Resolve or After Effects. |
| [GET](../api/#tag/compositions/get/renders/{id}) | `/renders/{id}` | Fetch one render, including its status, warnings, and produced asset id. |
| [POST](../api/#tag/compositions/post/renders/blocks) | `/renders/blocks` | Assemble ordered clip and narration pairs into one MP4. |
<!-- /gen -->

## Render

![A composition queues a render, which is polled until it produces an asset](../assets/diagrams/workflow-compositions.svg)

| Property | Value |
| --- | --- |
| Start | `POST /compositions/{id}/render` |
| Immediate response | `202` with a `Render`, not a generation `Job` |
| Poll | `GET /renders/{id}` |
| Statuses | `queued`, `running`, `succeeded`, `failed` |
| Default output | MP4 video asset |
| Still output | `target: still` produces an `image/png` asset |
| Result | `asset_id` on success; `error` on failure; inspect `warnings` either way |

Set `COMPOSITION_ID` to a composition you own. The request and response examples below are built from the schemas; they are not captured render runs.

```bash title="Queue an MP4 render — example"
$ curl -sS -X POST "https://api.nolgia.ai/v1/compositions/$COMPOSITION_ID/render" \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json title="202 — example from the Render schema"
{
  "id": "6250ce92-d911-4b47-9f0c-a884a481586f",
  "composition_id": "dcb61d59-59bc-41c1-b5cb-7b7adf22d02c",
  "user_id": "4961eaef-70a6-4e9b-b746-c86ad3e62077",
  "status": "queued",
  "params": {},
  "warnings": [],
  "created_at": "2026-09-21T04:00:00Z",
  "updated_at": "2026-09-21T04:00:00Z"
}
```

<!-- gen:fields schema=CreateRenderRequest -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `target` | `RenderTarget` | No |  |
| `still` | `RenderStillOptions` | No |  |
| `audio_mix` | `AudioMix` | No |  |
| `params` | object, nullable | No | Reserved for future render options; must be empty or omitted today. |
<!-- /gen -->

<!-- gen:fields schema=Render -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes |  |
| `composition_id` | string | Yes |  |
| `user_id` | string | Yes |  |
| `status` | `RenderStatus` | Yes |  |
| `params` | object | Yes | Empty for a video composition render.… |
| `asset_id` | string, nullable | No | The produced asset, a video for an MP4 render and an image/png for a still, set when status is succeeded. |
| `warnings` | array of string | Yes | Human-readable notes about skipped/unsupported timeline nodes and per-block narration speed-up or last-frame hold. |
| `error` | string, nullable | No | Failure reason, set when status is failed (truncated to 2000 characters). |
| `created_at` | string | Yes |  |
| `started_at` | string, nullable | No |  |
| `finished_at` | string, nullable | No |  |
| `updated_at` | string | Yes |  |
<!-- /gen -->

Save the returned `id` as `RENDER_ID` and poll until the render is terminal. A delayed worker trigger can leave it queued until the scheduled worker picks it up; keep polling the same render.

```bash title="Poll a render — example"
$ curl -sS "https://api.nolgia.ai/v1/renders/$RENDER_ID" \
  -H "Authorization: Bearer $NOLGIA_TOKEN"
```

```json title="200 — example of a completed Render"
{
  "id": "6250ce92-d911-4b47-9f0c-a884a481586f",
  "composition_id": "dcb61d59-59bc-41c1-b5cb-7b7adf22d02c",
  "user_id": "4961eaef-70a6-4e9b-b746-c86ad3e62077",
  "status": "succeeded",
  "params": {},
  "asset_id": "86997121-220d-4206-a70e-64ec8474c058",
  "warnings": [],
  "created_at": "2026-09-21T04:00:00Z",
  "started_at": "2026-09-21T04:00:02Z",
  "finished_at": "2026-09-21T04:00:30Z",
  "updated_at": "2026-09-21T04:00:30Z"
}
```

| Response field | Meaning |
| --- | --- |
| `id`, `composition_id`, `user_id` | Render, source composition and requester ids |
| `status` | Poll while `queued` or `running`; stop at `succeeded` or `failed` |
| `params` | Empty for a video composition render; records normalized still options for a still |
| `asset_id` | Read `GET /assets/{id}` for a fresh download URL; can be null if the output was later deleted |
| `warnings` | Notes about skipped or unsupported elements; success does not imply every timeline element was rendered |
| `created_at`, `started_at`, `finished_at`, `updated_at` | Render lifecycle timestamps |
| `error` | Failure reason when the render fails |

> [!WARNING]
> Rendering supports a documented timeline subset. It does not execute scripts or GSAP tweens, and authored CSS is not applied. Unsupported elements and unresolvable media are skipped with warnings. Read `warnings` and inspect the output before treating a successful render as an exact copy of the browser preview.

Supported output includes timed video, audio and images, Studio edit timing and track toggles, grades, masks, sampled clip motion, and supported text. Text additions use Studio `textStyle`; authored plain text uses the renderer's caption treatment. The API reference above describes the exact subset and its limits.

### Still frames

| Property | Value |
| --- | --- |
| Request | `target: still` plus optional `still` options |
| Frame selection | `at_seconds` or zero-based `frame`, never both; neither means frame 0 |
| Background | `opaque` by default; `transparent` preserves uncovered canvas alpha |
| Downscale | `max_dimension` from 64 to 3840 pixels |
| Resolution cap | Native still output is capped at 3840×2160; downscale larger canvases |
| Past the end | Renders the last frame and adds a warning |

```bash title="Queue a transparent still — example"
$ curl -sS "https://api.nolgia.ai/v1/compositions/$COMPOSITION_ID/render" \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"target":"still","still":{"at_seconds":2,"background":"transparent","max_dimension":1280}}'
```

```json title="202 — schema-built still render example"
{
  "id": "2cc38557-afc8-4dd2-8b3c-f335a36a4967",
  "composition_id": "dcb61d59-59bc-41c1-b5cb-7b7adf22d02c",
  "user_id": "4961eaef-70a6-4e9b-b746-c86ad3e62077",
  "status": "queued",
  "params": {"mode":"still","at_seconds":2,"background":"transparent","max_dimension":1280},
  "warnings": [],
  "created_at": "2026-09-21T04:01:00Z",
  "updated_at": "2026-09-21T04:01:00Z"
}
```

| Response field | Meaning |
| --- | --- |
| `id`, `composition_id`, `user_id` | Render, source and requester ids, as for video |
| `status` | Still renders use the same asynchronous lifecycle |
| `params.mode` | `still` identifies a single-frame render |
| `params.at_seconds`, `params.background`, `params.max_dimension` | The normalized options the worker will apply |
| `warnings` | Notes about unsupported content or clamping the requested timestamp |
| `created_at`, `updated_at` | Render timestamps |
| `asset_id` | Appears on success and points to the PNG asset |

<!-- gen:fields schema=RenderStillOptions -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `at_seconds` | number | No | Timestamp rounded to the nearest frame using the plan FPS in the worker, then clamped to the last frame with a warning when past the end.… |
| `frame` | integer | No | Zero-based frame index, clamped to the last frame with a warning when past the end. Cannot accompany at_seconds. Invalid values return 400 with "still.frame must be between 0 and 2592000". |
| `background` | one of `opaque`, `transparent` | No | Opaque includes the black backdrop; omitted means opaque.… |
| `max_dimension` | integer | No | Downscale the finished still so its long edge is at most this many pixels, preserving aspect ratio.… |
<!-- /gen -->

## Export

`GET /compositions/{id}/export?format=fcpxml|aejsx` synchronously returns a ZIP for an editor. It uses the same effective timeline as MP4 rendering.

### fcpxml

| Property | Value |
| --- | --- |
| Editors | Premiere Pro and DaVinci Resolve |
| Project file | `sequence.xml` |
| Actual interchange | Final Cut Pro 7 XML, `xmeml` version 5, 30 fps |
| Text | Text additions become PNG stills under `text/` |

### aejsx

| Property | Value |
| --- | --- |
| Editor | After Effects |
| Project file | `build.jsx` |
| Open | File → Scripts → Run Script File |
| Editable features | Native text layers, keyframed transforms and masks |

Both formats contain `luts/*.cube`, `README.txt` describing fidelity limits, and `manifest.json`. Authored HTML, CSS and GSAP animation do not map into either editor.

```bash title="Download an editor ZIP — example"
$ curl --fail-with-body -sS \
  "https://api.nolgia.ai/v1/compositions/$COMPOSITION_ID/export?format=fcpxml" \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -o composition.zip
```

The response is `200 application/zip` with an attachment disposition, not JSON. These are the documented fields in the ZIP's `manifest.json`:

| Manifest field | Meaning |
| --- | --- |
| `media[]` | Every referenced media file to download separately |
| `media[].ref` | The source media reference |
| `media[].asset_id` | The referenced platform asset id |
| `media[].path` | Destination path relative to the export folder |
| `media[].url` | Signed download URL, valid for one hour |
| `media[].expires_at` | Actual expiry of that download URL |

> [!WARNING]
> Media bytes are not in the ZIP. Download every `manifest.json` media URL into its listed path beside the project before opening it. Complete those downloads before the one-hour URLs expire.

Export returns `404` for an unknown or foreign composition, `400` for an unknown format, `422` for a timeline without exportable clips, and `413` when its documents exceed the export limits.

## Versions and Figma imports

| Surface | What it does | Trap |
| --- | --- | --- |
| `POST /compositions/{id}/clone` | Copies the full file bundle into a new composition; `meta_patch` shallow-merges metadata | `nolgia-edits.json` is deliberately omitted so the clone starts with a clean edit overlay |
| `meta.series` and `meta.version` | Group versions; filter with `GET /compositions?series=…` | Clients assign and increment versions; the API stores them verbatim |
| `POST /compositions/{id}/imports/figma` | Imports a frame as text-layer descriptions and PNG still assets | Returns layer descriptions without modifying the composition; the client writes its overlay |

Figma imports require a Figma PAT with `file_content:read`. It is used for that request only and is not stored or logged. An import supports at most 40 layers. The token belongs in the authenticated request body, never in a composition file or URL.

## Share a render

Use `POST /renders/{id}/share` once the render is `succeeded` and its output asset still exists. The link resolves to the rendered MP4; its preview uses the composition name. Read-only roles cannot share, organization members may share their own requested renders, and owners or admins may share any visible render.

```bash title="Share a finished render — example"
$ curl -sS "https://api.nolgia.ai/v1/renders/$RENDER_ID/share" \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"expires_in_days":30}'
```

This is a schema-built example; the token and URL are placeholders, not working links.

```json title="201 — example from the ShareLink schema"
{
  "id": "93b16566-1998-4fc0-8b1c-7e4c77e224ee",
  "kind": "render",
  "target_id": "6250ce92-d911-4b47-9f0c-a884a481586f",
  "token": "…",
  "token_prefix": "…",
  "url": "https://nolgia.ai/share/…",
  "expires_at": "2026-10-21T04:02:00Z",
  "created_at": "2026-09-21T04:02:00Z",
  "created_by": "4961eaef-70a6-4e9b-b746-c86ad3e62077",
  "access_count": 0
}
```

<!-- gen:fields schema=ShareLink -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes |  |
| `kind` | `ShareLinkKind` | Yes |  |
| `target_id` | string | Yes | The shared asset's id, or the shared render's id. |
| `token` | string | No | The share token (base64url, 128 bits of entropy). Create response only. |
| `token_prefix` | string | Yes | The first characters of the token, for recognising a link in a listing. |
| `url` | string | No | The public share URL to hand out (the nolgia.ai share page). Create response only. |
| `expires_at` | string | No | When the link stops resolving. Absent for a link that never expires. |
| `has_password` | boolean | No | Viewers must enter a password. |
| `view_only` | boolean | No | Downloads are turned off for this link. |
| `created_at` | string | Yes |  |
| `created_by` | string | Yes | The user who created the link. |
| `access_count` | integer | Yes | Successful public resolutions so far. |
| `last_accessed_at` | string | No | When the link was last resolved. Absent until the first visit. |
| `revoked_at` | string | No | Set once the link has been revoked. Listings only return unrevoked links. |
<!-- /gen -->

Save `url` when creating it: tokens and URLs are returned once. `GET /renders/{id}/share` lists active links without either secret; `DELETE /renders/{id}/share/{token}` accepts the share token or link id and revokes it. See [File access controls](./file-access.html) for expiry, public resolution and revocation behavior.

## Next steps

:::cards
- [Styles and looks](./styles.html): Choose the grades and looks that a composition renders.
- [Storage and data retention](./storage.html): Keep asset ids, refresh signed URLs and manage deletion.
:::
