# Productions

> How a production moves from brief to finished video, and every field you can send.

A production is one video, from the brief you send to the share link you get back. It is the same thing whether it starts from the REST API, the SDK, the CLI, the GitHub Action, an AI assistant or the Productions page in the app. The pitch always comes before any canvas work, so a misfired call only costs a free pitch.

## Lifecycle

| Stage | In the app | Meaning | Charged |
| --- | --- | --- | --- |
| `queued` | Queued | Created, waiting for the pitch to be written. | No |
| `writing_pitch` | Writing pitch | Raylight is writing the script and shot list. | No |
| `pitch_ready` | Pitch ready | Waiting for someone to greenlight it, send comments or decline. | No |
| `greenlit` | Greenlit | Credits charged, project created, waiting for a render machine. | Yes |
| `making` | In production | Raylight is building the video in a project in your workspace. | Yes |
| `exporting` | Exporting | The video is rendering. | Yes |
| `delivered` | Wrapped | Share link ready and pushed to you. | Yes |
| `skipped` | Skipped | Raylight judged the brief not worth a video. Terminal. | No |
| `declined` | Declined | Someone declined the pitch. Terminal. | No |
| `cancelled` | Cancelled | Cancelled before production started. Terminal. | No, or refunded in full if it was greenlit |
| `failed` | Failed | Production failed after two tries. Terminal; `error` says why. | Refunded in full |

Show the labels in the second column to people. Code should branch on the stage values, which never change. The SDK exports the same labels as `PRODUCTION_STAGE_LABELS`, and `productionStageLabel(stage)` returns the label, or the raw value for a stage newer than your SDK.

`GET /v1/productions/{id}` returns the `production`, its `events` (created, stage changes, each pitch version, comments, the greenlight, delivery) and, while greenlit, in production or exporting, a `queue` object: the `phase` it is in (`preparing` (Setting up), `planning` (Planning the video), `building` (Building scenes), `reviewing` (Checking the cut), `polishing` (Polishing), `exporting` (Exporting)) with `lastProgress` as its label to show, `startedAt` for when work on it started, and `retrying` when an earlier try failed and Raylight is trying again. Every event made by a person carries `actorName`, the name to show, next to the raw `actor`. `production.pageUrl` is the production's page in the app; link people there rather than building the URL yourself.

## The brief

Everything the pitch step needs. Only `intent` is required. The quickstart example:

```json
{
  "brief": {
    "intent": "Announce dark mode in the Acme dashboard to existing customers.",
    "context": "PR #412 adds a theme toggle to Settings. Charts, tables and the sidebar all pick up the dark palette. Ships to everyone on Thursday.",
    "media": [
      {
        "id": "dashboard-dark",
        "kind": "image",
        "url": "https://cdn.acme.dev/press/dashboard-dark.png",
        "name": "Dashboard in dark mode",
        "description": "The analytics overview with the new dark theme."
      }
    ],
    "code": [
      {
        "path": "src/components/settings/theme-toggle.tsx",
        "source": "export const ThemeToggle = () => { /* ... */ }"
      }
    ],
    "format": {
      "template": "feature",
      "durationMs": 10000,
      "aspect": "16:9",
      "sound": { "voiceover": true, "music": true, "sfx": true }
    },
    "brand": {
      "name": "Acme",
      "primaryColor": "#6C5CE7",
      "tone": "confident, plain"
    }
  },
  "greenlightMode": "manual",
  "externalId": "acme/web#412",
  "origin": {
    "provider": "github",
    "label": "acme/web #412",
    "url": "https://github.com/acme/web/pull/412",
    "ref": "feat/dark-mode",
    "requestedBy": "jane"
  }
}
```

### intent

One sentence, 1 to 2,000 characters: what the video is about and who it is for. "Announce dark mode to existing customers" beats "dark mode video"; the audience changes the pacing and the copy.

### context

Up to 60,000 characters: release notes, the PR body, linked issues, a diff summary, feature docs. Paste generously; the pitch only uses what the shots need. Source code goes in `code`, not here.

### media

Up to 20 images or videos by URL. Each item has an `id` (what the shot list's `mediaIds` refer to), a `kind` (`image` or `video`), a `url`, a `name` and an optional `description`. Media is fetched into the project's library when the video is made, so the URL must still be reachable from Raylight's servers then: public, or signed with an expiry that outlasts the wait for a greenlight (days, if your team is slow to answer). Screenshots and screen recordings of the real product work best.

### code

Up to 40 UI source files, 200 KB each. React with Tailwind in v1. Files are parsed, never run. Raylight rebuilds the component as native frames, text, shapes and icons, so the shot shows your real UI at hero scale instead of a screenshot. Each file has:

| Field | Type | Notes |
| --- | --- | --- |
| `path` | string | Repository path. Required. |
| `source` | string | The file contents. Required, up to 200,000 characters. |
| `status` | added, modified, removed, renamed | How the change touched the file. Null when handed in by hand. |
| `additions`, `deletions` | integer | Line counts from the diff. Default 0. |
| `patch` | string | The unified diff hunks, up to 20,000 characters. Never leaves Raylight again. |
| `digest` | string | A short description of how the UI looks, up to 1,500 characters. |
| `score` | 0 to 1 | How relevant the file is to a product video. |

**How ranking works.** Files are sorted by `score`. The top ones are read in full when the pitch is written; the rest are known only by their `digest`. The highest scoring component becomes the hero shot; helpers, hooks and stylesheets score low and do not become shots. The CLI (`raylight brief build`) and the GitHub Action compute `status`, `patch`, `digest` and `score` from the diff on your machine, so you can read the brief before it goes. A plain API call can send only `path` and `source`; Raylight derives digest and score when it writes the pitch. Send the components that render the feature, not the whole tree.

### format

| Field | Values | Default |
| --- | --- | --- |
| `template` | `changelog`, `launch`, `feature`, `digest`, `custom` | `changelog` |
| `durationMs` | 5,000 to 15,000 | 10,000 |
| `aspect` | `16:9`, `4:3`, `3:2`, `21:9`, `1:1`, `9:16`, `3:4`, `2:3`, `4:5` | `16:9` |
| `sound.voiceover`, `sound.music`, `sound.sfx` | boolean | all `true` |

The defaults are the every-release preset: a 10 second widescreen changelog with full sound. Productions are short on purpose: a feature recap or changelog explainer, delivered as a project you can open in Raylight and grow into a full launch video. `launch` is for a named release, `feature` for one capability, `digest` for a week of small changes, `custom` when the intent says it all.

### brand

`name`, `logoUrl`, `primaryColor`, `backgroundColor`, `fontFamily` and `tone` (a short voice note like "dry, technical"). All optional and nullable. Missing values come from the workspace (logo, fonts) and from the code you sent (colors), and all are editable on the production before it is greenlit.

## Pitches

The script and shot list for sign-off, in the production's `pitch` field. Each round of comments makes a new version; earlier versions stay in the production's events.

```json
{
  "version": 1,
  "verdict": "video",
  "verdictReason": null,
  "title": "Dark mode, finally",
  "summary": "A 10 second feature spot: the toggle flips, the dashboard follows, and an end card says it ships Thursday.",
  "shots": [
    {
      "index": 0,
      "title": "The flip",
      "durationMs": 3000,
      "beat": "The theme toggle flips and the whole settings page goes dark.",
      "visual": "The ThemeToggle component rebuilt as layers at hero scale, centred, a slow push-in. On the flip the background sweeps from white to the brand dark.",
      "text": "Dark mode",
      "voiceover": "Dark mode is here.",
      "mediaIds": [],
      "codePaths": ["src/components/settings/theme-toggle.tsx"]
    },
    {
      "index": 1,
      "title": "The dashboard",
      "durationMs": 4500,
      "beat": "The analytics overview in dark, tilted, drifting left.",
      "visual": "dashboard-dark on a device frame, 12 degree tilt, a lateral drift with a soft depth of field.",
      "text": "Every chart. Every table.",
      "voiceover": "Charts, tables and the sidebar all follow.",
      "mediaIds": ["dashboard-dark"],
      "codePaths": []
    },
    {
      "index": 2,
      "title": "Sign-off",
      "durationMs": 2500,
      "beat": "The Acme logo on the dark background with the ship date.",
      "visual": "Logo centred on the brand dark, the date types on underneath, a short hold before the cut.",
      "text": "Thursday, for everyone",
      "voiceover": "Turn it on in Settings, Thursday.",
      "mediaIds": [],
      "codePaths": []
    }
  ],
  "music": "Low, steady, electronic. No drop.",
  "estimatedCredits": 600,
  "createdAt": "2026-10-17T18:04:11.000Z"
}
```

- `verdict` is `video` or `skip`. A skip ends the production in `skipped` with `verdictReason` set, charges nothing, and fires `production.skipped`. Rework the brief and create again.
- Each shot has a `beat` (the one line a reader skims), a `visual` (the full direction for building it), on-screen `text`, a `voiceover` line, and the `mediaIds` and `codePaths` it shows. 1 to 24 shots.
- `estimatedCredits` is the price quoted with the pitch, and what the greenlight charges.
- `pitchMarkdown` (on `GET /v1/productions/{id}`, in webhook payloads, on greenlight links and in MCP results) is the same pitch formatted for reading.

### Sending comments

Send comments, get the next version, free. Be specific: shots to add or drop, copy to rewrite, pacing, tone. Pass `pitchVersion` so comments meant for a pitch that changed meanwhile are refused (409). A production gets 6 pitch versions in total, so 5 rounds of comments after the first pitch; after that it must be greenlit or declined, and another round answers 429 with `code: "cap"`. Each version counts as one pitch against the monthly cap.

```bash
curl -X POST https://api.raylight.app/v1/productions/6f1c2b7e-3d4a-4c8e-9a2b-1f0e5d6c7b8a/comments \
  -H "Authorization: Bearer $RAYLIGHT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "comment": "Open on the toggle, not the logo. Drop the pricing shot.", "pitchVersion": 1 }'
```

## Greenlight

| `greenlightMode` | Behaviour |
| --- | --- |
| `manual` (default) | The production waits at Pitch ready until someone greenlights it, over REST, MCP, the app or a signed link. |
| `auto` | Auto-greenlight: a `video` verdict is greenlit as soon as the pitch lands, and credits are charged. A `skip` still skips. If the automatic greenlight is refused (not enough credits), the production stays at Pitch ready with a note and the normal `production.pitch_ready` event fires. |

Pass `pitchVersion` when you greenlight to refuse it if the pitch changed since you read it (409 when stale). The production's `greenlight` records the mode, the `actor` (a user id, `link:<email>` for an email link, `webhook:<endpointId>` for a link from a webhook payload, or `auto`), an `actorName` to show people, the time and the pitch version greenlit. Cancel before production for a full refund; a production already being made cannot be cancelled.

## Origin

Where the production came from, in your words. Display only: the Productions page shows it and the emails quote it; Raylight never calls the provider. `provider` is a lowercase slug (`github`, `gitlab`, `vercel`, `linear`, `zapier`, `claude-code`), `label` is the line to show ("acme/web #412", "v2.3.0"), `url` makes it a link, `ref` is the branch, tag or commit, and `requestedBy` is the handle to credit.

## Idempotency

Pass `externalId` (up to 200 characters) on create. A repeat create with the same id from the same key owner returns the existing production with 200 instead of 201, so a retried CI step or replayed webhook never makes two pitches, and you can tell a replay from a new production by the status. Use something stable per unit of work: the PR number, the release tag, the deploy id.

## Errors

Every non-2xx body is `{ "error": "...", "code": "..." }`; validation failures add `issues`.

| Status | Code | When |
| --- | --- | --- |
| 400 |  | Invalid body or query; `issues` lists the fields. |
| 401 |  | Missing, invalid or revoked key. |
| 402 | `credits` | The workspace owner's credits do not cover the video: on create or comments, the length asked for; on greenlight, the pitch's `estimatedCredits`. Nothing changed; add credits (or ask for a shorter video) and try again. |
| 403 | `api_key_missing_scope` | The key lacks the `productions` scope. |
| 403 | `forbidden` | View-only access to this workspace: the key's owner can read it but not change it. |
| 404 | `not_found` | No such production in a workspace you belong to. |
| 409 | `stage` | The production's stage does not allow the action, or `pitchVersion` is stale. |
| 429 | `cap`, `rate_limited` | A monthly cap is reached, the pitch is out of rounds of comments, or more than 120 requests a minute on this key. Rate limits set `Retry-After`. |
| 503 | `disabled` | Productions are switched off on this server. |

## Caps

Every plan can make productions, Free included, with no monthly limit on videos. Pitches are free but capped per calendar month, so a noisy trigger cannot pile up pitches nobody reads; the plan sets that cap. Each video costs 60 credits per second of video (300 to 900 credits); the pitch quotes the exact number as `estimatedCredits`. A pitch is only written when the workspace owner's credits cover the video it asks for, so a workspace with no credits gets a 402 instead of a pitch nobody can greenlight. `GET /v1/productions/usage` returns this month's counts and the pitch cap. Numbers on [Limits and pricing](https://www.raylight.app/docs/limits.md).

## More

- Previous: [Quickstart](https://www.raylight.app/docs/quickstart.md)
- Next: [Webhooks](https://www.raylight.app/docs/webhooks.md)
- All docs: https://www.raylight.app/llms.txt
- Everything in one file: https://www.raylight.app/llms-full.txt
- OpenAPI: https://api.raylight.app/v1/openapi.json
