---
title: "Upload a screenplay."
method: POST
path: "/scripts"
tags: ["Scripts"]
---

# Upload a screenplay.

`POST /scripts`

## Request

- Base URL: `https://docs.sleeperhit.studio/api/v1`
- URL: `https://docs.sleeperhit.studio/api/v1/scripts`
- Auth: HTTP bearer

The Writers Lab's upload, through its core: a new script WITH ITS PROJECT — it joins the project named in `projectId` (400 if that project is not the writer's live one; 409 `project_precondition_failed` with the gate's reason while that project cannot start an episode, as for POST /story-projects/{projectId}/episodes), or gets a new project named after the script — with its first version, parsed and its characters analysed in the background (read GET /scripts/{scriptId} until `parseStatus` is `complete`). With `projectId`, `seasonNumber` files it in that season (a planned episode already says its own season), and `episodeId` links it to a planned episode of the project's Series Bible by that stable id — refused with 400 `validation_failed` for an episode no longer in the Bible, and with 409 `script_state_invalid` when that episode already has a script (upload a new version of THAT script with POST /scripts/{scriptId}/versions instead); a title or label matching exactly one UNWRITTEN planned episode links by id on its own. A new screenplay can come from a file (a PDF, a Final Draft .fdx, or a Fountain or plain-text script) instead of its text. The file belongs to a project, so `projectId` is required: `fileUrl` (a link to the file) adds it and answers `waiting` with its `sourceId`; check that source until it is READY, then send `sourceId` and its text becomes the new screenplay. A source that is already READY becomes the screenplay at once, and one source makes its screenplay once (sending it again is refused, naming the script it already made). The body goes through the same parser as the Writers Lab, so a bad title, content or format is refused with the same sentence. Refused with 403 `plan_limit_reached` past the plan's page allowance. Free. Requires `Idempotency-Key`.

## Headers

- `Idempotency-Key` string, required

## Request body

- ScriptUploadRequest: Exactly one of `content`, `sourceId` and `fileUrl`. A new screenplay can come from a file (a PDF, a Final Draft .fdx, or a Fountain or plain-text script) instead of its text. The file belongs to a project, so `projectId` is required: `fileUrl` (a link to the file) adds it and answers `waiting` with its `sourceId`; check that source until it is READY, then send `sourceId` and its text becomes the new screenplay. A source that is already READY becomes the screenplay at once, and one source makes its screenplay once (sending it again is refused, naming the script it already made).
  - `title` string, required
  - `content` string: The screenplay text.
  - `sourceId` string: A source of the named project whose text becomes the screenplay once it is READY. Needs `projectId`.
  - `fileUrl` string, uri: A link to the file (PDF, .fdx, .fountain or .txt), read in the background as a source of the named project. Needs `projectId`.
  - `fileName` string: The file's name; its extension says what it is.
  - `format` 'text' | 'fountain' | 'fdx': How the text is written (default `text`; for a file, its own else `text`).
  - `projectId` string: The project to file the script in; required for a file source, and without it a new project is named after the script.
  - `seasonNumber` integer: The season to file the script in (a planned episode already says its own season). Needs `projectId`.
  - `episodeId` string: The Series Bible planned-episode id this upload IS (`plannedEpisodes[].episodeId` from GET /story-projects/{projectId}/episodes): the script is linked to it by id. Needs `projectId`.

## Response `200`

The new script and its first version, or `waiting` while its file is still being read.

- union
  - ScriptUploadResponse
    - `script` WriterScript, required
      - `scriptId` string, required
      - `routeId` string, required: The id in its Writers Lab URL.
      - `projectId` string, nullable, required: Its project, or null for a script in no project.
      - `title` string, required: The stored title.
      - `label` string, required: The name the episode is listed by everywhere, e.g. "S1E1 — The Return": its season and number with its title (a leading project name and episode code taken off); a script in no project is its title. Display only.
      - `slug` string, nullable, required: Its URL slug.
      - `logline` string, nullable, required: The writer's one-sentence logline — the same one Slug and the Writers Lab read and write.
      - `mediaType` string, nullable, required: `film` or `tv`.
      - `genre` string, nullable, required: Free text, in the writer's words.
      - `seasonNumber` integer, required
      - `createdAt` string, date-time, required
      - `updatedAt` string, date-time, required
      - `workspacePath` string, required: Where it opens in the Writers Lab, relative to the site origin.
      - `versions` WriterScriptVersion[], required: Newest first.
        - `scriptUploadId` string, required: The version's id: what coverage, overrides, table reads and exports take.
        - `versionNumber` integer, required
        - `title` string, required
        - `status` string, required: `draft` while it is being written; otherwise a locked version.
        - `parseStatus` string, nullable, required: Where the screenplay parse stands (`queued`, `parsing`, `complete`, `failed`); null when none ran.
        - `slug` string, nullable, required
        - `wordCount` integer, required
        - `pageCount` integer, nullable, required
        - `format` string, required: How it was written: text, fountain or fdx.
        - `createdAt` string, date-time, required
        - `counts` object, required: How much has been made from this version.
          - `coverageReports` integer, required
          - `tableReadJobs` integer, required
          - `pitchDeckJobs` integer, required
          - `visualizerJobs` integer, required
    - `scriptUploadId` string, required: The version this call created.
    - `versionNumber` integer, required
  - ScriptUploadWaiting: A new screenplay from a file is still being read: nothing is made yet. Check the source until it is READY, then send `sourceId`.
    - `waiting` true, required
    - `title` string, required: The title the new screenplay will carry.
    - `projectId` string, required: The project the source is in (GET /story-projects/{projectId}/sources/{sourceId}).
    - `sourceId` string, required: Send it as `sourceId` once the source is READY.
    - `sourceStatus` 'PENDING' | 'EXTRACTING', required
    - `message` string, required: What to tell the writer, and how to go on.

## Other responses

- `400`: Request payload failed validation. The error message names the offending field.
- `401`: Missing, malformed, revoked, or expired API key.
- `403`: API key is valid but lacks the scope, is blocked by IP allowlist, or the account is disabled.
- `409`: The project cannot start an episode yet (`project_precondition_failed`, `details.reason`), or the named planned episode is already written (`script_state_invalid`, `details.scriptId` names the script to upload a new version of).
- `429`: Rate limit for this API key + IP + route family was exceeded.
- `500`: The API failed unexpectedly. Safe to retry with the same Idempotency-Key.

---

[API](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api.md) · [All operations](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api/llms.txt) · [OpenAPI document](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api/revisions/64c1102ccb2f?raw)
