---
title: "Store a tour for one feature of the project."
method: POST
path: "/story-projects/{projectId}/tours"
tags: ["Sources"]
---

# Store a tour for one feature of the project.

`POST /story-projects/{projectId}/tours`

## Request

- Base URL: `https://docs.sleeperhit.studio/api/v1`
- URL: `https://docs.sleeperhit.studio/api/v1/story-projects/{projectId}/tours`
- Auth: HTTP bearer

Binds the tour document (`tour`, stored as written) to one of the project's features (`featureId`, a Series Bible episode), with an optional `label`; the tour's name is taken from the document itself. A document the recorder could not run is refused VERBATIM with its problems (400 `validation_failed`, `details.errors`) and nothing is stored; the answer's `check` re-runs that check. 409 `tour_limit_reached` past 200 (`TOURS_PER_PROJECT_MAX`) tours per project. Requires `Idempotency-Key`. Free.

## Headers

- `Idempotency-Key` string, required

## Request body

- TourCreate: A tour bound to one feature of the project. The document is stored as written; its name is taken from it.
  - `featureId` string, required: The feature (a Series Bible episode) the tour walks through.
  - `tour` object, required: The tour document. Refused (`validation_failed`, `details.errors`) with the recorder's own problems; nothing stored.
  - `label` string: Free text beside the tour on every surface.

## Response `200`

The stored tour.

- TourEnvelope
  - `tour` Tour, required: A stored tour. Its document is held as the caller wrote it, and its check is re-run on every read.
    - `id` string, required
    - `projectId` string, required
    - `featureId` string, required: The product feature (a Series Bible episode) it walks through.
    - `feature` object, nullable, required: The feature as the map shows it (title, else label, else the id), or null when the Bible lists it no more — then `featureMissing` is true and the row is kept.
      - `id` string, required
      - `name` string, required
    - `featureMissing` boolean, required
    - `label` string, nullable, required
    - `name` string, required: Named from the tour document itself, so every surface calls it what it walks through.
    - `tour` object, required: The document as the caller wrote it (at most 65536 bytes of JSON).
    - `revision` integer, required: Moves on with every change; send it back as `expectedRevision` to refuse a write over a second writer.
    - `check` TourCheckResult, required: What the document check answers about one tour: the same problems `capture record`'s own check raises, verbatim — plus what recording this tour needs and what it hears.
      - `ok` boolean, required: True when the tour can be recorded (`errors` is empty).
      - `errors` string[], required: One readable line per problem, exactly as the recorder's check says them.
      - `warnings` string[], required: What is true but needs saying (app sound, secrets to resolve). Never a refusal.
      - `secretRefs` string[], required: The `secretRef` ids the tour's type steps name: a recording must resolve each one.
      - `sound` 'off' | 'app', nullable, required: What the take hears; null when the tour does not parse.
      - `stepCount` integer, nullable, required: How many recording steps the tour has; null when it does not parse.
    - `drafted` object, nullable, required: The drafted provenance of the tour, or null for a tour written by hand: the run that drafted it, when it started drafting, and what the draft held back (its assumptions and questions).
      - `runId` string, required: The tour-draft run that staged this tour.
      - `draftedAt` string, date-time, required: When the run started drafting.
      - `assumptions` string[], required: What the draft assumed about the app it never opened.
      - `questions` string[], required: What the draft could not know and asks the writer.
    - `recording` object, nullable, required: Where the tour's newest bought recording stands, or null when no recording was ever bought or made. A price that was never bought is not a recording.
      - `jobId` string, nullable, required: The bought recording job it comes from (read it with GET .../recordings/{jobId}); null for a recording made by hand for this tour.
      - `state` 'booked' | 'recording' | 'recorded' | 'failed' | 'cancelled', required: booked: bought, waiting for a recording box. recording: running now. recorded: done, with its capture. failed or cancelled: record it again.
      - `sentence` string, required: Where it stands, in the words every surface prints: why it failed and whether its credits went back.
      - `at` string, date-time, required: When it reached this state.
      - `captureId` string, nullable, required: The tour's newest ready recording, or null when it has none yet.
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required

## 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.
- `404`: The named project or source does not exist or is owned by another account.
- `409`: This project already holds 200 tours, the most it can (`tour_limit_reached`, `details.limit`). Delete one you no longer use, then create again. Nothing was created.
- `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)
