# openapi.yaml — GENERATED from packages/core/src/api/spec.ts by `pnpm --filter @script-rip/core gen:openapi`.
# Do not edit by hand: docs/spec/07-api.md is authoritative, spec.ts transcribes it, this file serialises spec.ts.
# CI fails when this file is stale (07 §13.1) and when any path here is absent from 02 §8.1 or vice versa.
openapi: 3.1.0
info:
  title: script-rip
  version: 1.0.0
  description: Turns recorded speech into a usable transcript. Give it a YouTube link, an audio or video file, or a recording; poll the run or register a destination; fetch the transcript in one of six formats. This document is generated from packages/core/src/api/spec.ts, which transcribes docs/spec/07-api.md; 07 is authoritative.
  x-spec: docs/spec/07-api.md
servers:
  - url: https://{host}/v1
    variables:
      host:
        default: script.rip
        description: The deployment's host. Every example in the documentation writes it as <host>.
security:
  - bearerAuth: []
paths:
  /runs:
    post:
      operationId: runs_create
      summary: Submit a run
      description: |-
        Creates a run from one of the three source shapes. A YouTube run is queued; an upload or recording run is receiving until its upload completes. A run served from the cached lane comes back done in this very response with status 200, because no work was created.

        Specified in 07 §8.1.

        **Rate limit.** Per key, 10 per minute (burst 20) — submission spends GPU seconds.
      tags:
        - runs
      x-scopes:
        - runs:write
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 255
          description: A value you will not reuse by accident — a UUID. A repeat within 24 hours returns the original response with its original status code (07 §3).
      requestBody:
        required: true
        description: Unknown fields are rejected with 400 invalid_request naming the key (07 §1.5).
        content:
          application/json:
            schema:
              type: object
              properties:
                source:
                  oneOf:
                    - type: object
                      properties:
                        type:
                          type: string
                          const: youtube
                        url:
                          type: string
                          description: Any YouTube URL shape, or a bare 11-character video id (06 §2.1).
                      required:
                        - type
                        - url
                      additionalProperties: false
                    - type: object
                      properties:
                        type:
                          type: string
                          const: upload
                        upload_id:
                          type: string
                          format: uuid
                      required:
                        - type
                        - upload_id
                      additionalProperties: false
                    - type: object
                      properties:
                        type:
                          type: string
                          const: recording
                        upload_id:
                          type: string
                          format: uuid
                      required:
                        - type
                        - upload_id
                      additionalProperties: false
                  description: Exactly one of the three shapes. Two, or none, is 400 invalid_request.
                name_speakers:
                  type: boolean
                  description: Default true. What is asked for; a single-speaker recording skips naming and says so in the step detail.
                requested_language:
                  type:
                    - string
                    - "null"
                  description: Pin ASR to a language. null (the default) is auto-detect. A pinned language bypasses the cached lane.
                force:
                  type: boolean
                  description: Default false. true skips the cached lane and runs the engine — "transcribe it again" is a real request.
                idempotency_key:
                  type:
                    - string
                    - "null"
                  minLength: 1
                  maxLength: 255
                  description: An alternative to the header. If both are present and differ, 400 invalid_request.
              required:
                - source
              additionalProperties: false
            example:
              source:
                type: youtube
                url: https://www.youtube.com/watch?v=dQw4w9WgXcQ
              name_speakers: true
              requested_language: null
              force: false
              idempotency_key: null
      responses:
        "200":
          description: Served from the cached lane — done already, lane cached, cached_from_run_id naming the source. Or an idempotent repeat of a cached submission.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Run"
        "201":
          description: The run was created.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Run"
              example:
                id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
                source_type: youtube
                source_url: https://www.youtube.com/watch?v=dQw4w9WgXcQ
                source_upload_id: null
                title: null
                author_name: null
                author_url: null
                description: null
                published_on: null
                duration_seconds: null
                thumbnail_url: null
                status: queued
                lane: null
                engine: null
                current_step: null
                step_log: []
                language: null
                requested_language: null
                name_speakers: true
                segments: null
                segment_count: null
                speaker_count: null
                word_count: null
                speaker_labels: null
                cached_from_run_id: null
                media_fingerprint: dQw4w9WgXcQ
                error: null
                error_class: null
                retry_after: null
                idempotency_key: 4f0a9e21-59f2-4d64-8e18-7d9b0b1c66aa
                created_at: 2026-03-11T18:04:11Z
                updated_at: 2026-03-11T18:04:11Z
                completed_at: null
                transcript_url: null
                expected_steps: null
                seconds_until_modal: 79
                live_workers:
                  count: 2
                  kind: utility
                worker_label: null
                machine_quiet: false
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableSource"
        "429":
          description: 429 — one of RateLimited, QuotaExceeded; see components.responses for each. The `code` field says which.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
            Retry-After:
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/EngineUnavailable"
    get:
      operationId: runs_list
      summary: List runs
      description: |-
        Cursor-paginated, newest first, scoped to your account. segments is absent from every element in every status — fetch one run, or fetch its transcript.

        Specified in 07 §8.2.
      tags:
        - runs
      x-scopes:
        - runs:read
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
          description: Default 25, in [1, 100]. Outside the range is 400 invalid_request, never clamped.
        - name: cursor
          in: query
          required: false
          schema:
            type: string
          description: Opaque, from the previous page's next_cursor. Never construct one.
        - name: status
          in: query
          required: false
          schema:
            type: array
            items:
              $ref: "#/components/schemas/RunStatus"
          description: Repeatable; values union. A value outside 02 §4.2 is 400.
          style: form
          explode: true
        - name: source_type
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/SourceType"
      responses:
        "200":
          description: A page.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RunListPage"
              example:
                data:
                  - id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
                    source_type: youtube
                    source_url: https://www.youtube.com/watch?v=dQw4w9WgXcQ
                    source_upload_id: null
                    title: The Long Way South — Part 4
                    author_name: Sailing Somewhere
                    author_url: https://www.youtube.com/@sailingsomewhere
                    description: null
                    published_on: 2026-03-04
                    duration_seconds: 2711.5
                    thumbnail_url: https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg
                    status: done
                    lane: modal
                    engine: whisperx large-v2
                    current_step: null
                    step_log:
                      - step: fetch
                        started_at: 2026-03-11T18:04:22Z
                        ended_at: 2026-03-11T18:05:09Z
                        ok: true
                        detail: 45.2 MB in 47 s
                      - step: normalize
                        started_at: 2026-03-11T18:05:09Z
                        ended_at: 2026-03-11T18:05:18Z
                        ok: true
                        detail: 2711.5 s · 16 kHz mono
                      - step: transcribe
                        started_at: 2026-03-11T18:05:18Z
                        ended_at: 2026-03-11T18:07:44Z
                        ok: true
                        detail: 412 segments · language en
                      - step: align
                        started_at: 2026-03-11T18:07:44Z
                        ended_at: 2026-03-11T18:08:31Z
                        ok: true
                        detail: word-level timestamps for 412 segments
                      - step: diarize
                        started_at: 2026-03-11T18:08:31Z
                        ended_at: 2026-03-11T18:09:40Z
                        ok: true
                        detail: 2 speakers over 412 segments
                      - step: name_speakers
                        started_at: 2026-03-11T18:09:40Z
                        ended_at: 2026-03-11T18:09:55Z
                        ok: true
                        detail: named 2 of 2
                      - step: finalize
                        started_at: 2026-03-11T18:09:55Z
                        ended_at: 2026-03-11T18:09:57Z
                        ok: true
                        detail: 412 segments · 6841 words
                    language: en
                    requested_language: null
                    name_speakers: true
                    segment_count: 412
                    speaker_count: 2
                    word_count: 6841
                    speaker_labels:
                      SPEAKER_00:
                        name: Matt
                        role: host
                      SPEAKER_01:
                        name: Amy
                        role: guest
                    cached_from_run_id: null
                    media_fingerprint: dQw4w9WgXcQ
                    error: null
                    error_class: null
                    retry_after: null
                    idempotency_key: 4f0a9e21-59f2-4d64-8e18-7d9b0b1c66aa
                    created_at: 2026-03-11T18:04:11Z
                    updated_at: 2026-03-11T18:09:57Z
                    completed_at: 2026-03-11T18:09:57Z
                    transcript_url: https://<host>/v1/runs/0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b/transcript
                    expected_steps:
                      - fetch
                      - normalize
                      - transcribe
                      - align
                      - diarize
                      - name_speakers
                      - finalize
                    seconds_until_modal: null
                    live_workers: null
                    worker_label: null
                    machine_quiet: false
                has_more: true
                next_cursor: eyJjIjoiMjAyNi0wMy0xMVQxODowNDoxMVoiLCJpIjoiMDE5NWM4ZTQtOGQwMi03YzE5LWIzZTctNWExZjlkMjBjNjhiIn0
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /runs/{id}:
    get:
      operationId: runs_get
      summary: One run
      description: |-
        The polling endpoint. segments and speaker_labels are populated only when status is done. Poll no faster than every 2 seconds, back off to 10 seconds after a minute — or register a destination and do not poll at all.

        Specified in 07 §8.3.

        **This read writes.** Runs the stale sweep and the Modal fallback clock — age-guarded, idempotent, and only while somebody is actually waiting (02 §8.1).
      tags:
        - runs
      x-scopes:
        - runs:read
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The run id.
      responses:
        "200":
          description: The run.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Run"
              example:
                id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
                source_type: youtube
                source_url: https://www.youtube.com/watch?v=dQw4w9WgXcQ
                source_upload_id: null
                title: The Long Way South — Part 4
                author_name: Sailing Somewhere
                author_url: https://www.youtube.com/@sailingsomewhere
                description: null
                published_on: 2026-03-04
                duration_seconds: 2711.5
                thumbnail_url: https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg
                status: done
                lane: modal
                engine: whisperx large-v2
                current_step: null
                step_log:
                  - step: fetch
                    started_at: 2026-03-11T18:04:22Z
                    ended_at: 2026-03-11T18:05:09Z
                    ok: true
                    detail: 45.2 MB in 47 s
                  - step: normalize
                    started_at: 2026-03-11T18:05:09Z
                    ended_at: 2026-03-11T18:05:18Z
                    ok: true
                    detail: 2711.5 s · 16 kHz mono
                  - step: transcribe
                    started_at: 2026-03-11T18:05:18Z
                    ended_at: 2026-03-11T18:07:44Z
                    ok: true
                    detail: 412 segments · language en
                  - step: align
                    started_at: 2026-03-11T18:07:44Z
                    ended_at: 2026-03-11T18:08:31Z
                    ok: true
                    detail: word-level timestamps for 412 segments
                  - step: diarize
                    started_at: 2026-03-11T18:08:31Z
                    ended_at: 2026-03-11T18:09:40Z
                    ok: true
                    detail: 2 speakers over 412 segments
                  - step: name_speakers
                    started_at: 2026-03-11T18:09:40Z
                    ended_at: 2026-03-11T18:09:55Z
                    ok: true
                    detail: named 2 of 2
                  - step: finalize
                    started_at: 2026-03-11T18:09:55Z
                    ended_at: 2026-03-11T18:09:57Z
                    ok: true
                    detail: 412 segments · 6841 words
                language: en
                requested_language: null
                name_speakers: true
                segments:
                  - start: 0.48
                    end: 4.02
                    text: Right, so this is day nine.
                    speaker: SPEAKER_00
                  - start: 4.02
                    end: 7.61
                    text: Day nine and we still haven't seen land.
                    speaker: SPEAKER_00
                  - start: 8.1
                    end: 11.44
                    text: I keep telling him that's the point.
                    speaker: SPEAKER_01
                segment_count: 412
                speaker_count: 2
                word_count: 6841
                speaker_labels:
                  SPEAKER_00:
                    name: Matt
                    role: host
                  SPEAKER_01:
                    name: Amy
                    role: guest
                cached_from_run_id: null
                media_fingerprint: dQw4w9WgXcQ
                error: null
                error_class: null
                retry_after: null
                idempotency_key: 4f0a9e21-59f2-4d64-8e18-7d9b0b1c66aa
                created_at: 2026-03-11T18:04:11Z
                updated_at: 2026-03-11T18:09:57Z
                completed_at: 2026-03-11T18:09:57Z
                transcript_url: https://<host>/v1/runs/0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b/transcript
                expected_steps:
                  - fetch
                  - normalize
                  - transcribe
                  - align
                  - diarize
                  - name_speakers
                  - finalize
                seconds_until_modal: null
                live_workers: null
                worker_label: null
                machine_quiet: false
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      operationId: runs_delete
      summary: Delete a run and its media
      description: |-
        Deletes the row and purges its media immediately. A run in flight is cancelled first, in the same transaction, so run.canceled fires before the row disappears. Always 200 with a receipt, never 204; a second delete is 404.

        Specified in 07 §8.4.
      tags:
        - runs
      x-scopes:
        - runs:write
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The run id.
      responses:
        "200":
          description: A receipt.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  deleted:
                    type: boolean
                  media_objects_purged:
                    type: integer
                  message:
                    type: string
                required:
                  - id
                  - deleted
                  - media_objects_purged
                  - message
              example:
                id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
                deleted: true
                media_objects_purged: 1
                message: The run and 1 media object were deleted.
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /runs/{id}/cancel:
    post:
      operationId: runs_cancel
      summary: Stop a run in flight
      description: |-
        Empty body. Cancellation is cooperative at the worker's next step boundary; a step in flight is not interrupted. run.canceled is enqueued in the same transaction. A run already terminal is 409 conflict with its actual status in detail.

        Specified in 07 §8.5.
      tags:
        - runs
      x-scopes:
        - runs:write
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The run id.
      responses:
        "200":
          description: The run, now terminal, plus a sentence naming what to expect.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  source_type:
                    $ref: "#/components/schemas/SourceType"
                  source_url:
                    type:
                      - string
                      - "null"
                    format: uri
                    description: Always null for upload and recording.
                  source_upload_id:
                    type:
                      - string
                      - "null"
                    format: uuid
                    description: Always null for youtube; null after the upload row is deleted.
                  title:
                    type:
                      - string
                      - "null"
                    description: null until acquisition learns it. Never invented.
                  author_name:
                    type:
                      - string
                      - "null"
                  author_url:
                    type:
                      - string
                      - "null"
                    format: uri
                  description:
                    type:
                      - string
                      - "null"
                  published_on:
                    type:
                      - string
                      - "null"
                    format: date
                    description: A bare calendar date.
                  duration_seconds:
                    type:
                      - number
                      - "null"
                    format: double
                    description: null until normalize establishes it.
                  thumbnail_url:
                    type:
                      - string
                      - "null"
                    format: uri
                  status:
                    $ref: "#/components/schemas/RunStatus"
                  lane:
                    $ref: "#/components/schemas/Lane"
                    description: null until a lane takes it. modal_pending is a guard value, not a lane.
                  engine:
                    type:
                      - string
                      - "null"
                    description: A human string ("whisperx large-v2"), not an enumeration. Do not branch on it.
                  current_step:
                    oneOf:
                      - $ref: "#/components/schemas/Step"
                      - type: "null"
                  step_log:
                    type: array
                    items:
                      $ref: "#/components/schemas/StepEntry"
                  language:
                    type:
                      - string
                      - "null"
                    description: BCP-47-ish; null until ASR detects it.
                  requested_language:
                    type:
                      - string
                      - "null"
                  name_speakers:
                    type: boolean
                    description: What was asked for, not what happened.
                  segments:
                    type:
                      - array
                      - "null"
                    items:
                      $ref: "#/components/schemas/Segment"
                    description: null until status is done — "not yet", never "there is none". Never present in GET /v1/runs or in a webhook body.
                  segment_count:
                    type:
                      - integer
                      - "null"
                  speaker_count:
                    type:
                      - integer
                      - "null"
                    description: "0 is a real value: diarization produced nothing."
                  word_count:
                    type:
                      - integer
                      - "null"
                  speaker_labels:
                    type:
                      - object
                      - "null"
                    additionalProperties:
                      $ref: "#/components/schemas/SpeakerLabel"
                    description: Keyed by speaker id (SPEAKER_00, …). null until done, and on a done run where nothing was named.
                  cached_from_run_id:
                    type:
                      - string
                      - "null"
                    format: uuid
                    description: Set only when lane is cached.
                  media_fingerprint:
                    type:
                      - string
                      - "null"
                    description: The canonical video id, or the media sha256.
                  error:
                    type:
                      - string
                      - "null"
                    description: A sentence for a person. Programs branch on error_class.
                  error_class:
                    oneOf:
                      - $ref: "#/components/schemas/ErrorClass"
                      - type: "null"
                  retry_after:
                    type:
                      - string
                      - "null"
                    format: date-time
                    description: "Set on error_class premiere: when to submit a NEW run."
                  idempotency_key:
                    type:
                      - string
                      - "null"
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
                  completed_at:
                    type:
                      - string
                      - "null"
                    format: date-time
                    description: Non-null on every terminal status.
                  transcript_url:
                    type:
                      - string
                      - "null"
                    format: uri
                    description: Derived. The absolute GET /v1/runs/{id}/transcript URL; null unless done.
                  expected_steps:
                    type:
                      - array
                      - "null"
                    items:
                      $ref: "#/components/schemas/Step"
                    description: Derived. null while lane is null or modal_pending — draw no checklist.
                  seconds_until_modal:
                    type:
                      - integer
                      - "null"
                    description: Derived. null unless queued with no lane and the countdown is running.
                  live_workers:
                    type:
                      - object
                      - "null"
                    properties:
                      count:
                        type: integer
                      kind:
                        $ref: "#/components/schemas/WorkerRole"
                    required:
                      - count
                      - kind
                    description: Derived. null unless the countdown is running.
                  worker_label:
                    type:
                      - string
                      - "null"
                    description: Derived. The machine's display label; null unless a fleet worker holds the run. claimed_by is never sent.
                  machine_quiet:
                    type: boolean
                    description: Derived, never null. True only for a fleet run whose worker has stopped heartbeating; always false for modal.
                  message:
                    type: string
                required:
                  - id
                  - status
                  - error_class
                  - message
              example:
                id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
                source_type: youtube
                source_url: https://www.youtube.com/watch?v=dQw4w9WgXcQ
                source_upload_id: null
                title: null
                author_name: null
                author_url: null
                description: null
                published_on: null
                duration_seconds: null
                thumbnail_url: null
                status: canceled
                lane: modal
                engine: whisperx large-v2
                current_step: null
                step_log: []
                language: null
                requested_language: null
                name_speakers: true
                segments: null
                segment_count: null
                speaker_count: null
                word_count: null
                speaker_labels: null
                cached_from_run_id: null
                media_fingerprint: dQw4w9WgXcQ
                error: Stopped by a person.
                error_class: canceled
                retry_after: null
                idempotency_key: 4f0a9e21-59f2-4d64-8e18-7d9b0b1c66aa
                created_at: 2026-03-11T18:04:11Z
                updated_at: 2026-03-11T18:06:02Z
                completed_at: 2026-03-11T18:06:02Z
                transcript_url: null
                expected_steps:
                  - fetch
                  - normalize
                  - transcribe
                  - align
                  - diarize
                  - name_speakers
                  - finalize
                seconds_until_modal: null
                live_workers: null
                worker_label: null
                machine_quiet: false
                message: Stopped. The GPU finishes what it is in the middle of and shuts down; you are not charged for the rest.
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /runs/{id}/transcript:
    get:
      operationId: runs_transcript
      summary: The transcript, in one format, as text
      description: |-
        Six renderings of one segments array. A run that is not done is 409 conflict with its status in detail. ?download=1 adds a Content-Disposition attachment header.

        Specified in 07 §8.6.
      tags:
        - runs
      x-scopes:
        - runs:read
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The run id.
        - name: format
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/OutputFormat"
          description: Default speakers.
        - name: download
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            maximum: 1
          description: 1 to receive it as an attachment named after the title.
      responses:
        "200":
          description: text/plain for prose, speakers and timestamped; application/x-subrip for srt; text/vtt for vtt; application/json for json.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            text/plain; charset=utf-8:
              schema:
                type: string
              example: |
                Matt: Right, so this is day nine. Day nine and we still haven't seen land.

                Amy: I keep telling him that's the point.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /runs/{id}/deliver:
    post:
      operationId: runs_deliver
      summary: Send this run to a destination now
      description: |-
        Re-delivery for a program: enqueues a delivery on the same retry ladder as the one the terminal transition enqueued. Requires runs:read AND destinations:write together (02 §8). A run that is not terminal, or a destination that is disabled, is 409 conflict.

        Specified in 07 §8.7.
      tags:
        - runs
      x-scopes:
        - runs:read
        - destinations:write
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The run id.
      requestBody:
        required: true
        description: Unknown fields are rejected with 400 invalid_request naming the key (07 §1.5).
        content:
          application/json:
            schema:
              type: object
              properties:
                destination_id:
                  type: string
                  format: uuid
                  description: Must belong to your account; one that does not is 404.
                event:
                  $ref: "#/components/schemas/WebhookEvent"
                  description: Defaults to the event the run's terminal status implies.
              required:
                - destination_id
              additionalProperties: false
            example:
              destination_id: 6d2b8a10-1f47-4c8e-a2f0-cb9e4d3a7711
      responses:
        "202":
          description: Queued.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                type: object
                properties:
                  delivery_id:
                    type: string
                    format: uuid
                  destination_id:
                    type: string
                    format: uuid
                  run_id:
                    type: string
                    format: uuid
                  event:
                    $ref: "#/components/schemas/WebhookEvent"
                  status:
                    $ref: "#/components/schemas/DeliveryStatus"
                  message:
                    type: string
                required:
                  - delivery_id
                  - destination_id
                  - run_id
                  - event
                  - status
                  - message
              example:
                delivery_id: 8c41d0aa-63e2-4a52-9f77-2b1c0e8d4a90
                destination_id: 6d2b8a10-1f47-4c8e-a2f0-cb9e4d3a7711
                run_id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
                event: run.completed
                status: pending
                created: true
                message: Queued for delivery. The sender runs every minute; the first attempt goes out on its next pass.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /uploads:
    post:
      operationId: uploads_create
      summary: Open an upload and get a transfer target
      description: |-
        No endpoint accepts media bytes: every byte moves directly to storage over a short-lived presigned URL. declared_bytes is required for upload and omitted for recording; sha256 is optional for upload and forbidden for recording. create_run defaults true and creates the linked run in status receiving in the same transaction.

        Specified in 07 §9.2, 06 §3.3.
      tags:
        - uploads
      x-scopes:
        - uploads:write
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 255
          description: A value you will not reuse by accident — a UUID. A repeat within 24 hours returns the original response with its original status code (07 §3).
      requestBody:
        required: true
        description: Unknown fields are rejected with 400 invalid_request naming the key (07 §1.5).
        content:
          application/json:
            schema:
              type: object
              properties:
                kind:
                  type: string
                  enum:
                    - upload
                    - recording
                filename:
                  type: string
                  description: Advisory; becomes the run title after sanitising. Never decides media type.
                content_type:
                  type: string
                  description: Advisory, recorded for diagnostics, never trusted.
                declared_bytes:
                  type: integer
                  format: int64
                  minimum: 1
                  description: Required for upload, omitted for recording. Checked against the byte ceiling before any URL is issued.
                sha256:
                  type: string
                  pattern: ^[0-9a-f]{64}$
                  description: Optional for upload; forbidden for recording. Lets the cached lane answer before a byte moves.
                part_size:
                  type: integer
                  description: Requested part size in bytes; clamped to [5 MiB, 64 MiB] for multipart.
                create_run:
                  type: boolean
                  description: Default true.
                run:
                  type: object
                  properties:
                    name_speakers:
                      type: boolean
                    requested_language:
                      type:
                        - string
                        - "null"
                  required: []
                  additionalProperties: false
              required:
                - kind
              additionalProperties: false
            example:
              kind: upload
              filename: board-meeting.m4a
              content_type: audio/mp4
              declared_bytes: 148234901
              create_run: true
      responses:
        "201":
          description: The upload is open. For storage_mode single the one transfer target is inline.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadOpened"
              example:
                upload_id: 0b5f2e63-83a1-4a30-9a35-64d2f0f9a7ab
                kind: upload
                status: open
                storage_mode: multipart
                part_size: 8388608
                max_parts: 10000
                expires_at: 2026-03-12T18:04:11Z
                run_id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
                limits:
                  max_run_bytes: 2147483648
                  max_run_seconds: 28800
                target: null
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          description: 429 — one of RateLimited, QuotaExceeded; see components.responses for each. The `code` field says which.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
            Retry-After:
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/EngineUnavailable"
  /uploads/{id}:
    get:
      operationId: uploads_get
      summary: State of an upload
      description: |-
        What a client asks after a crash. received_part_indexes is the complete list, so reconciliation is set arithmetic.

        Specified in 07 §9.3.
      tags:
        - uploads
      x-scopes:
        - uploads:write
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The upload id.
      responses:
        "200":
          description: The upload.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadState"
              example:
                upload_id: 0b5f2e63-83a1-4a30-9a35-64d2f0f9a7ab
                kind: recording
                status: open
                storage_mode: parts
                part_count: 41
                received_bytes: 4975104
                received_part_indexes:
                  - 1
                  - 2
                  - 3
                  - 4
                  - 5
                  - 6
                  - 7
                  - 8
                  - 9
                  - 10
                  - 11
                  - 12
                  - 13
                  - 14
                  - 15
                  - 16
                  - 17
                  - 18
                  - 19
                  - 20
                  - 21
                  - 22
                  - 23
                  - 24
                  - 25
                  - 26
                  - 27
                  - 28
                  - 29
                  - 30
                  - 31
                  - 32
                  - 33
                  - 34
                  - 35
                  - 36
                  - 37
                  - 38
                  - 39
                  - 40
                  - 41
                highest_contiguous_part: 41
                has_more_parts: false
                run_id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
                created_at: 2026-03-11T17:12:04Z
                expires_at: 2026-03-12T17:12:04Z
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      operationId: uploads_abort
      summary: Abort
      description: |-
        Aborting is never an error: 200 whether this request moved it or found it already aborted or expired. A complete upload cannot be aborted: 409 conflict. A linked run in receiving becomes canceled.

        Specified in 07 §9.6, 06 §3.6.
      tags:
        - uploads
      x-scopes:
        - uploads:write
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The upload id.
      responses:
        "200":
          description: The state, and whether this request changed it.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadAborted"
              example:
                upload_id: 0b5f2e63-83a1-4a30-9a35-64d2f0f9a7ab
                status: aborted
                run:
                  id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
                  status: canceled
                message: The upload was aborted and its run was canceled.
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /uploads/{id}/parts:
    post:
      operationId: uploads_parts
      summary: Get transfer targets, or record parts that landed
      description: |-
        One endpoint, two operations, told apart by the presence of received. part_index is 1-based and contiguous; at most 100 targets per request; a target expires in 15 minutes. sha256 is required on every reported part; etag only in multipart mode.

        Specified in 07 §9.4, 06 §3.4.

        **Rate limit.** Per account, 600 per minute — a recorder draining a backlog is a legitimate burst.
      tags:
        - uploads
      x-scopes:
        - uploads:write
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The upload id.
      requestBody:
        required: true
        description: Unknown fields are rejected with 400 invalid_request naming the key (07 §1.5).
        content:
          application/json:
            schema:
              type: object
              properties:
                parts:
                  type: array
                  items:
                    type: object
                    properties:
                      part_index:
                        type: integer
                        minimum: 1
                      bytes:
                        type: integer
                        format: int64
                        minimum: 1
                      sha256:
                        type: string
                        pattern: ^[0-9a-f]{64}$
                      etag:
                        type: string
                      received:
                        type: boolean
                    required:
                      - part_index
                    additionalProperties: false
                  minItems: 1
                  maxItems: 100
              required:
                - parts
              additionalProperties: false
            example:
              parts:
                - part_index: 1
                  bytes: 8388608
                  sha256: 3f79bb7b435b05321651daefd374cdc681dc06faa65e374e38337b88ca046dea
                  etag: '"9b2cf535f27731c974343645a3985328"'
                  received: true
      responses:
        "200":
          description: Targets, when the request asked for them; progress, when it recorded landed parts.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/UploadTargets"
                  - $ref: "#/components/schemas/UploadProgress"
              example:
                upload_id: 0b5f2e63-83a1-4a30-9a35-64d2f0f9a7ab
                status: open
                part_count: 1
                received_bytes: 8388608
                highest_contiguous_part: 1
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /uploads/{id}/complete:
    post:
      operationId: uploads_complete
      summary: Finish and verify
      description: |-
        One statement with every guard in its predicate. The run becomes queued. A retried completion promotes nothing a second time and says so. 409 conflict names the actual cause: already complete, aborted, expired, a part-count mismatch, or a gap at index n.

        Specified in 07 §9.5, 06 §3.5.
      tags:
        - uploads
      x-scopes:
        - uploads:write
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The upload id.
      requestBody:
        required: true
        description: Unknown fields are rejected with 400 invalid_request naming the key (07 §1.5).
        content:
          application/json:
            schema:
              type: object
              properties:
                part_count:
                  type: integer
                  minimum: 1
                sha256:
                  type: string
                  pattern: ^[0-9a-f]{64}$
                  description: Over the concatenated media in part order. Required for recording, optional for upload.
                duration_seconds:
                  type: number
                  format: double
                  minimum: 0
                  description: Advisory; lets the duration ceiling be enforced now. Overwritten by the measured value.
              required:
                - part_count
              additionalProperties: false
            example:
              part_count: 18
              sha256: b1946ac92492d2347c6235b4d2611184c4d3f0a1e6f2b8c9d0e1f2a3b4c5d6e7
              duration_seconds: 3612
      responses:
        "200":
          description: Verified; the run is queued.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadCompleted"
              example:
                upload_id: 0b5f2e63-83a1-4a30-9a35-64d2f0f9a7ab
                status: complete
                part_count: 18
                received_bytes: 148234901
                sha256: b1946ac92492d2347c6235b4d2611184c4d3f0a1e6f2b8c9d0e1f2a3b4c5d6e7
                run:
                  id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
                  source_type: upload
                  source_url: null
                  source_upload_id: 0b5f2e63-83a1-4a30-9a35-64d2f0f9a7ab
                  title: null
                  author_name: null
                  author_url: null
                  description: null
                  published_on: null
                  duration_seconds: null
                  thumbnail_url: null
                  status: queued
                  lane: null
                  engine: null
                  current_step: null
                  step_log: []
                  language: null
                  requested_language: null
                  name_speakers: true
                  segments: null
                  segment_count: null
                  speaker_count: null
                  word_count: null
                  speaker_labels: null
                  cached_from_run_id: null
                  media_fingerprint: b1946ac92492d2347c6235b4d2611184c4d3f0a1e6f2b8c9d0e1f2a3b4c5d6e7
                  error: null
                  error_class: null
                  retry_after: null
                  idempotency_key: null
                  created_at: 2026-03-11T18:04:11Z
                  updated_at: 2026-03-11T18:04:11Z
                  completed_at: null
                  transcript_url: null
                  expected_steps: null
                  seconds_until_modal: 79
                  live_workers:
                    count: 2
                    kind: utility
                  worker_label: null
                  machine_quiet: false
                message: 18 parts verified. The run is queued.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/UnprocessableSource"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /destinations:
    get:
      operationId: destinations_list
      summary: List destinations
      description: |-
        Every destination for the account, newest first, in the one list envelope. The ceiling is 20 per account so it is never paginated in practice.

        Specified in 07 §10.3.
      tags:
        - destinations
      x-scopes:
        - destinations:read
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
          description: Default 25, in [1, 100]. Outside the range is 400 invalid_request, never clamped.
        - name: cursor
          in: query
          required: false
          schema:
            type: string
          description: Opaque, from the previous page's next_cursor. Never construct one.
      responses:
        "200":
          description: A page.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DestinationListPage"
              example:
                data:
                  - id: 6d2b8a10-1f47-4c8e-a2f0-cb9e4d3a7711
                    name: bullshit.doctor
                    url: https://bullshit.doctor/hooks/script-rip
                    events:
                      - run.completed
                      - run.failed
                    enabled: true
                    disabled_reason: null
                    created_at: 2026-03-01T09:14:02Z
                    updated_at: 2026-03-11T18:10:00Z
                    last_delivery:
                      event: run.completed
                      status: delivered
                      status_code: 200
                      attempt: 1
                      delivered_at: 2026-03-11T18:09:59Z
                has_more: false
                next_cursor: null
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      operationId: destinations_create
      summary: Register a destination
      description: |-
        The secret is in this response and never again. The URL must be https:// and must resolve to a public address; loopback, private, link-local, carrier-grade NAT, unique-local and cloud-metadata addresses are 403 forbidden at registration and re-checked at every send. Above 20 destinations is 429 quota_exceeded.

        Specified in 07 §10.2.
      tags:
        - destinations
      x-scopes:
        - destinations:write
      requestBody:
        required: true
        description: Unknown fields are rejected with 400 invalid_request naming the key (07 §1.5).
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                  description: For a person reading a list.
                url:
                  type: string
                  format: uri
                  description: https:// only, and it must resolve to a public address; otherwise 403 forbidden (07 §4.2).
                events:
                  type: array
                  items:
                    $ref: "#/components/schemas/WebhookEvent"
                  minItems: 1
                  description: Default ["run.completed"]. A non-empty subset of 02 §4.6.
                secret:
                  type: string
                  minLength: 32
                  maxLength: 128
                  description: Supply your own, or omit and one is generated.
              required:
                - name
                - url
              additionalProperties: false
            example:
              name: bullshit.doctor
              url: https://bullshit.doctor/hooks/script-rip
              events:
                - run.completed
                - run.failed
      responses:
        "201":
          description: Registered. secret appears here once.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DestinationWithSecret"
              example:
                id: 6d2b8a10-1f47-4c8e-a2f0-cb9e4d3a7711
                name: bullshit.doctor
                url: https://bullshit.doctor/hooks/script-rip
                events:
                  - run.completed
                  - run.failed
                enabled: true
                disabled_reason: null
                created_at: 2026-03-01T09:14:02Z
                updated_at: 2026-03-01T09:14:02Z
                last_delivery: null
                secret: whsec_7Kq2mVx9RnB4tLpC0eZaW1sYdH6gJ3fU
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          description: 429 — one of RateLimited, QuotaExceeded; see components.responses for each. The `code` field says which.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
            Retry-After:
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          $ref: "#/components/responses/InternalError"
  /destinations/{id}:
    patch:
      operationId: destinations_update
      summary: Update a destination
      description: |-
        Any of name, url, events, enabled; absent fields are unchanged. {"rotate_secret": true} — optionally with secret — returns the new secret once; there is no dual-secret window. Re-enabling replays nothing. Disabling keeps the delivery history.

        Specified in 07 §10.4.
      tags:
        - destinations
      x-scopes:
        - destinations:write
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The destination id.
      requestBody:
        required: false
        description: Unknown fields are rejected with 400 invalid_request naming the key (07 §1.5).
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                url:
                  type: string
                  format: uri
                  description: https:// only; re-validated like a registration.
                events:
                  type: array
                  items:
                    $ref: "#/components/schemas/WebhookEvent"
                  minItems: 1
                enabled:
                  type: boolean
                rotate_secret:
                  type: boolean
                  description: true to issue a new secret, returned once in this response.
                secret:
                  type: string
                  minLength: 32
                  maxLength: 128
                  description: "With rotate_secret: supply your own."
              required: []
              additionalProperties: false
            example:
              enabled: false
      responses:
        "200":
          description: The destination. Carries secret only when it was rotated.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  name:
                    type: string
                  url:
                    type: string
                    format: uri
                    description: https:// only.
                  events:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookEvent"
                    minItems: 1
                  enabled:
                    type: boolean
                  disabled_reason:
                    type:
                      - string
                      - "null"
                    description: Derived. A sentence when the destination was disabled automatically after five consecutive exhausted deliveries (07 §12.5); null otherwise.
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
                  last_delivery:
                    oneOf:
                      - $ref: "#/components/schemas/LastDelivery"
                      - type: "null"
                  secret:
                    type: string
                    description: Present only when rotate_secret was true. Shown once.
                required:
                  - id
                  - name
                  - url
                  - events
                  - enabled
                  - disabled_reason
                  - created_at
                  - updated_at
                  - last_delivery
              example:
                id: 6d2b8a10-1f47-4c8e-a2f0-cb9e4d3a7711
                name: bullshit.doctor
                url: https://bullshit.doctor/hooks/script-rip
                events:
                  - run.completed
                  - run.failed
                enabled: false
                disabled_reason: null
                created_at: 2026-03-01T09:14:02Z
                updated_at: 2026-03-11T18:10:00Z
                last_delivery:
                  event: run.completed
                  status: delivered
                  status_code: 200
                  attempt: 1
                  delivered_at: 2026-03-11T18:09:59Z
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      operationId: destinations_delete
      summary: Remove a destination
      description: |-
        Deliveries cascade, so the history goes with it — which is why enabled: false exists for the case where the history is worth keeping. 200 with a receipt; a second delete is 404.

        Specified in 07 §10.5.
      tags:
        - destinations
      x-scopes:
        - destinations:write
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The destination id.
      responses:
        "200":
          description: A receipt.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  deleted:
                    type: boolean
                  deliveries_removed:
                    type: integer
                  message:
                    type: string
                required:
                  - id
                  - deleted
                  - deliveries_removed
                  - message
              example:
                id: 6d2b8a10-1f47-4c8e-a2f0-cb9e4d3a7711
                deleted: true
                deliveries_removed: 412
                message: The destination and its 412 delivery records were removed.
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /destinations/{id}/test:
    post:
      operationId: destinations_test
      summary: Send a signed test delivery
      description: |-
        Sent inline; the result is the response. The body is a complete, correctly signed payload carrying a synthetic run whose id is the all-zero UUID and whose test field is true. It writes no delivery record. A non-2xx from the receiver, a DNS failure, a TLS failure or a timeout is still 200 here with delivered: false — the test succeeded in telling you what happened.

        Specified in 07 §10.6.

        **Rate limit.** Per key, 6 per minute — human-triggered.
      tags:
        - destinations
      x-scopes:
        - destinations:write
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The destination id.
      requestBody:
        required: false
        description: Unknown fields are rejected with 400 invalid_request naming the key (07 §1.5).
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  $ref: "#/components/schemas/WebhookEvent"
                  description: Default run.completed. Test another event's shape.
              required: []
              additionalProperties: false
            example: {}
      responses:
        "200":
          description: What happened.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                type: object
                properties:
                  destination_id:
                    type: string
                    format: uuid
                  event:
                    $ref: "#/components/schemas/WebhookEvent"
                  delivered:
                    type: boolean
                  status_code:
                    type:
                      - integer
                      - "null"
                  latency_ms:
                    type: integer
                  signature_header:
                    type: string
                    description: The exact X-ScriptRip-Signature that was sent, so you can verify your verifier.
                  body_sha256:
                    type: string
                    description: sha256 of the exact bytes sent.
                  error:
                    type:
                      - string
                      - "null"
                    description: Which of DNS, TLS, timeout, redirect or refused-address failed, when delivered is false and there was no status code.
                  message:
                    type: string
                required:
                  - destination_id
                  - event
                  - delivered
                  - status_code
                  - latency_ms
                  - signature_header
                  - body_sha256
                  - error
                  - message
              example:
                destination_id: 6d2b8a10-1f47-4c8e-a2f0-cb9e4d3a7711
                event: run.completed
                delivered: true
                status_code: 200
                latency_ms: 214
                signature_header: t=1773252601,v1=1f8ac10f23c5b5bc1167bda84b833e5c057a77d2ef2f3f2b6b0c1d4e5a6b7c8d
                body_sha256: 9c1185a5c5e9fc54612808977ee8f548b2258d31f5a2a0e9b7c3d4e5f6a7b8c9
                error: null
                message: Delivered. The receiver answered 200 in 214 ms.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /destinations/{id}/deliveries:
    get:
      operationId: destinations_deliveries
      summary: What we sent and what came back
      description: |-
        Cursor paginated, newest first. The audit answer, where last_delivery on the destination is the health answer. A receiver that was down reads this to find what it missed, then pulls those runs by id or asks for them again with POST /v1/runs/{id}/deliver.

        Specified in 07 §10.7.
      tags:
        - destinations
      x-scopes:
        - destinations:read
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The destination id.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
          description: Default 25, in [1, 100]. Outside the range is 400 invalid_request, never clamped.
        - name: cursor
          in: query
          required: false
          schema:
            type: string
          description: Opaque, from the previous page's next_cursor. Never construct one.
        - name: status
          in: query
          required: false
          schema:
            type: array
            items:
              $ref: "#/components/schemas/DeliveryStatus"
          description: Repeatable; values union. ?status=exhausted is the question an operator actually has.
          style: form
          explode: true
        - name: event
          in: query
          required: false
          schema:
            type: array
            items:
              $ref: "#/components/schemas/WebhookEvent"
          description: Repeatable; values union.
          style: form
          explode: true
      responses:
        "200":
          description: A page.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            Cache-Control:
              description: Always no-store on an authenticated response.
              schema:
                type: string
                const: no-store
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryListPage"
              example:
                data:
                  - id: 8c41d0aa-63e2-4a52-9f77-2b1c0e8d4a90
                    run_id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
                    event: run.completed
                    attempt: 3
                    status: delivered
                    status_code: 200
                    error: null
                    next_attempt_at: null
                    delivered_at: 2026-03-11T18:14:22Z
                    created_at: 2026-03-11T18:09:58Z
                has_more: false
                next_cursor: null
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /health:
    get:
      operationId: health_get
      summary: Liveness and a build marker
      description: |-
        No authentication, no scope. Writes nothing: no sweep, no fallback clock, no usage event. Its database probe is a SELECT 1 on its own connection; a failure is 503 naming which check failed and never why. Rate-limited per IP.

        Specified in 07 §11.
      tags:
        - health
      x-scopes: []
      security: []
      responses:
        "200":
          description: Alive.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  version:
                    type: string
                    const: v1
                  time:
                    type: string
                    format: date-time
                  build:
                    type: string
                    description: The deployed build marker — a commit or build id (02 §8.1).
                required:
                  - ok
                  - version
                  - time
              example:
                ok: true
                version: v1
                time: 2026-03-11T18:11:44Z
                build: "070e121"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          description: A dependency failed. Which one, never why.
          headers:
            X-Request-Id:
              description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
              schema:
                type: string
            RateLimit-Limit:
              description: "07 §5.3: the refill rate per window."
              schema:
                type: integer
            RateLimit-Remaining:
              description: Tokens left in the bucket with fewer remaining.
              schema:
                type: integer
            RateLimit-Reset:
              description: Delta seconds until the bucket is full.
              schema:
                type: integer
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  db:
                    type: boolean
                required:
                  - ok
              example:
                ok: false
                db: false
tags:
  - name: runs
    description: The unit of work, the unit of billing, the row a client polls, the thing a webhook is about.
  - name: uploads
    description: Bytes move directly to storage over presigned URLs; this API only registers what landed.
  - name: destinations
    description: Where finished transcripts are pushed, signed. bullshit.doctor is a row here.
  - name: health
    description: Liveness. Writes nothing.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_live_<prefix>_<secret>
      description: "An API key (07 §2.1). Scopes are per key; a missing scope is 403, never 404. Bearer beats cookie: if both are present the key's scopes apply and the session is ignored."
    cookieAuth:
      type: apiKey
      in: cookie
      name: __Secure-scriptrip.session-token
      description: The web app's session. Programs use bearerAuth.
  schemas:
    SourceType:
      type: string
      enum:
        - youtube
        - upload
        - recording
      description: 02 §4.1 `source_type`.
    RunStatus:
      type: string
      enum:
        - receiving
        - queued
        - claimed
        - running
        - done
        - failed
        - canceled
      description: 02 §4.2 `run_status`.
    Lane:
      type:
        - string
        - "null"
      enum:
        - fleet
        - modal
        - modal_pending
        - cached
      description: 02 §4.3 `lane`.
    Step:
      type: string
      enum:
        - fetch
        - normalize
        - transcribe
        - vad
        - align
        - diarize
        - name_speakers
        - finalize
        - cached
      description: 02 §4.4 `step`.
    ErrorClass:
      type: string
      enum:
        - bot_wall
        - proxy
        - forbidden
        - gated
        - removed
        - premiere
        - truncated
        - unsupported_media
        - no_audio
        - too_long
        - too_large
        - corrupt_media
        - incomplete_upload
        - engine
        - stalled
        - budget
        - quota
        - canceled
        - other
      description: 02 §4.5 `error_class`.
    WebhookEvent:
      type: string
      enum:
        - run.completed
        - run.failed
        - run.canceled
      description: 02 §4.6 `webhook_event`.
    ApiErrorCode:
      type: string
      enum:
        - invalid_request
        - unauthenticated
        - forbidden
        - not_found
        - conflict
        - payload_too_large
        - unsupported_media_type
        - unprocessable_source
        - rate_limited
        - quota_exceeded
        - internal_error
        - engine_unavailable
      description: 02 §4.7 `api_error_code`.
    Role:
      type: string
      enum:
        - user
        - admin
      description: 02 §4.8 `role`. Operational; never appears on /v1's wire. Transcribed so 07 criterion 7 can assert set equality against the constitution in both directions.
    WorkerRole:
      type: string
      enum:
        - transcription
        - utility
      description: 02 §4.8 `worker_role`. Operational; never appears on /v1's wire. Transcribed so 07 criterion 7 can assert set equality against the constitution in both directions.
    UploadStatus:
      type: string
      enum:
        - open
        - complete
        - aborted
        - expired
      description: 02 §4.8 `upload_status`.
    StorageMode:
      type: string
      enum:
        - single
        - multipart
        - parts
      description: 02 §4.8 `storage_mode`.
    DeliveryStatus:
      type: string
      enum:
        - pending
        - delivered
        - exhausted
      description: 02 §4.8 `delivery_status`.
    OutputFormat:
      type: string
      enum:
        - prose
        - speakers
        - timestamped
        - srt
        - vtt
        - json
      description: 02 §4.8 `output_format`.
    CostSource:
      type: string
      enum:
        - billed
        - live
        - estimated
        - none
      description: 02 §4.8 `cost_source`. Operational; never appears on /v1's wire. Transcribed so 07 criterion 7 can assert set equality against the constitution in both directions.
    WorkerMode:
      type: string
      enum:
        - normal
        - polite
        - deferential
      description: 02 §4.8 `worker_mode`. Operational; never appears on /v1's wire. Transcribed so 07 criterion 7 can assert set equality against the constitution in both directions.
    UsageKind:
      type: string
      enum:
        - transcribed_seconds
        - cached_seconds
        - run_created
        - upload_bytes
      description: 02 §4.8 `usage_kind`. Operational; never appears on /v1's wire. Transcribed so 07 criterion 7 can assert set equality against the constitution in both directions.
    LlmCaller:
      type: string
      enum:
        - name_speakers
      description: 02 §4.8 `llm_caller`. Operational; never appears on /v1's wire. Transcribed so 07 criterion 7 can assert set equality against the constitution in both directions.
    ApiKeyScope:
      type: string
      enum:
        - runs:read
        - runs:write
        - uploads:write
        - destinations:read
        - destinations:write
      description: 02 §4.8 `api_key_scope`.
    Segment:
      type: object
      properties:
        start:
          type: number
          format: double
          description: Seconds from the start of the media.
        end:
          type: number
          format: double
        text:
          type: string
          minLength: 1
        speaker:
          type:
            - string
            - "null"
          pattern: ^SPEAKER_\d{2,}$
          description: Present on every segment; null where diarization had nothing to say.
      required:
        - start
        - end
        - text
        - speaker
      description: 04-engine §1.2. One ASR output span.
    StepEntry:
      type: object
      properties:
        step:
          $ref: "#/components/schemas/Step"
        started_at:
          type: string
          format: date-time
        ended_at:
          type:
            - string
            - "null"
          format: date-time
          description: null while the step is open — and a killed worker leaves exactly this.
        ok:
          type:
            - boolean
            - "null"
        detail:
          type:
            - string
            - "null"
      required:
        - step
        - started_at
        - ended_at
        - ok
        - detail
      description: 07 §7.3. Append-then-close.
    SpeakerLabel:
      type: object
      properties:
        name:
          type:
            - string
            - "null"
        role:
          type:
            - string
            - "null"
      required:
        - name
        - role
    Run:
      type: object
      properties:
        id:
          type: string
          format: uuid
        source_type:
          $ref: "#/components/schemas/SourceType"
        source_url:
          type:
            - string
            - "null"
          format: uri
          description: Always null for upload and recording.
        source_upload_id:
          type:
            - string
            - "null"
          format: uuid
          description: Always null for youtube; null after the upload row is deleted.
        title:
          type:
            - string
            - "null"
          description: null until acquisition learns it. Never invented.
        author_name:
          type:
            - string
            - "null"
        author_url:
          type:
            - string
            - "null"
          format: uri
        description:
          type:
            - string
            - "null"
        published_on:
          type:
            - string
            - "null"
          format: date
          description: A bare calendar date.
        duration_seconds:
          type:
            - number
            - "null"
          format: double
          description: null until normalize establishes it.
        thumbnail_url:
          type:
            - string
            - "null"
          format: uri
        status:
          $ref: "#/components/schemas/RunStatus"
        lane:
          $ref: "#/components/schemas/Lane"
          description: null until a lane takes it. modal_pending is a guard value, not a lane.
        engine:
          type:
            - string
            - "null"
          description: A human string ("whisperx large-v2"), not an enumeration. Do not branch on it.
        current_step:
          oneOf:
            - $ref: "#/components/schemas/Step"
            - type: "null"
        step_log:
          type: array
          items:
            $ref: "#/components/schemas/StepEntry"
        language:
          type:
            - string
            - "null"
          description: BCP-47-ish; null until ASR detects it.
        requested_language:
          type:
            - string
            - "null"
        name_speakers:
          type: boolean
          description: What was asked for, not what happened.
        segments:
          type:
            - array
            - "null"
          items:
            $ref: "#/components/schemas/Segment"
          description: null until status is done — "not yet", never "there is none". Never present in GET /v1/runs or in a webhook body.
        segment_count:
          type:
            - integer
            - "null"
        speaker_count:
          type:
            - integer
            - "null"
          description: "0 is a real value: diarization produced nothing."
        word_count:
          type:
            - integer
            - "null"
        speaker_labels:
          type:
            - object
            - "null"
          additionalProperties:
            $ref: "#/components/schemas/SpeakerLabel"
          description: Keyed by speaker id (SPEAKER_00, …). null until done, and on a done run where nothing was named.
        cached_from_run_id:
          type:
            - string
            - "null"
          format: uuid
          description: Set only when lane is cached.
        media_fingerprint:
          type:
            - string
            - "null"
          description: The canonical video id, or the media sha256.
        error:
          type:
            - string
            - "null"
          description: A sentence for a person. Programs branch on error_class.
        error_class:
          oneOf:
            - $ref: "#/components/schemas/ErrorClass"
            - type: "null"
        retry_after:
          type:
            - string
            - "null"
          format: date-time
          description: "Set on error_class premiere: when to submit a NEW run."
        idempotency_key:
          type:
            - string
            - "null"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        completed_at:
          type:
            - string
            - "null"
          format: date-time
          description: Non-null on every terminal status.
        transcript_url:
          type:
            - string
            - "null"
          format: uri
          description: Derived. The absolute GET /v1/runs/{id}/transcript URL; null unless done.
        expected_steps:
          type:
            - array
            - "null"
          items:
            $ref: "#/components/schemas/Step"
          description: Derived. null while lane is null or modal_pending — draw no checklist.
        seconds_until_modal:
          type:
            - integer
            - "null"
          description: Derived. null unless queued with no lane and the countdown is running.
        live_workers:
          type:
            - object
            - "null"
          properties:
            count:
              type: integer
            kind:
              $ref: "#/components/schemas/WorkerRole"
          required:
            - count
            - kind
          description: Derived. null unless the countdown is running.
        worker_label:
          type:
            - string
            - "null"
          description: Derived. The machine's display label; null unless a fleet worker holds the run. claimed_by is never sent.
        machine_quiet:
          type: boolean
          description: Derived, never null. True only for a fleet run whose worker has stopped heartbeating; always false for modal.
      required:
        - id
        - source_type
        - status
        - step_log
        - name_speakers
        - created_at
        - updated_at
        - machine_quiet
      description: 07 §7 — the run resource. Every field is a runs column except the derived ones, which say so.
    WebhookRun:
      type: object
      properties:
        id:
          type: string
          format: uuid
        source_type:
          $ref: "#/components/schemas/SourceType"
        source_url:
          type:
            - string
            - "null"
          format: uri
          description: Always null for upload and recording.
        title:
          type:
            - string
            - "null"
          description: null until acquisition learns it. Never invented.
        author_name:
          type:
            - string
            - "null"
        author_url:
          type:
            - string
            - "null"
          format: uri
        published_on:
          type:
            - string
            - "null"
          format: date
          description: A bare calendar date.
        duration_seconds:
          type:
            - number
            - "null"
          format: double
          description: null until normalize establishes it.
        thumbnail_url:
          type:
            - string
            - "null"
          format: uri
        status:
          $ref: "#/components/schemas/RunStatus"
        lane:
          $ref: "#/components/schemas/Lane"
          description: null until a lane takes it. modal_pending is a guard value, not a lane.
        engine:
          type:
            - string
            - "null"
          description: A human string ("whisperx large-v2"), not an enumeration. Do not branch on it.
        language:
          type:
            - string
            - "null"
          description: BCP-47-ish; null until ASR detects it.
        name_speakers:
          type: boolean
          description: What was asked for, not what happened.
        segment_count:
          type:
            - integer
            - "null"
        speaker_count:
          type:
            - integer
            - "null"
          description: "0 is a real value: diarization produced nothing."
        word_count:
          type:
            - integer
            - "null"
        speaker_labels:
          type:
            - object
            - "null"
          additionalProperties:
            $ref: "#/components/schemas/SpeakerLabel"
          description: Keyed by speaker id (SPEAKER_00, …). null until done, and on a done run where nothing was named.
        cached_from_run_id:
          type:
            - string
            - "null"
          format: uuid
          description: Set only when lane is cached.
        error:
          type:
            - string
            - "null"
          description: A sentence for a person. Programs branch on error_class.
        error_class:
          oneOf:
            - $ref: "#/components/schemas/ErrorClass"
            - type: "null"
        retry_after:
          type:
            - string
            - "null"
          format: date-time
          description: "Set on error_class premiere: when to submit a NEW run."
        created_at:
          type: string
          format: date-time
        completed_at:
          type:
            - string
            - "null"
          format: date-time
          description: Non-null on every terminal status.
        transcript_url:
          type:
            - string
            - "null"
          format: uri
          description: Derived. The absolute GET /v1/runs/{id}/transcript URL; null unless done.
      required:
        - id
        - source_type
        - source_url
        - title
        - author_name
        - author_url
        - published_on
        - duration_seconds
        - thumbnail_url
        - status
        - lane
        - engine
        - language
        - name_speakers
        - segment_count
        - speaker_count
        - word_count
        - speaker_labels
        - cached_from_run_id
        - error
        - error_class
        - retry_after
        - created_at
        - completed_at
        - transcript_url
      description: 07 §12.3. The run resource without segments, step_log or the in-flight derived fields. segments is NEVER in a webhook body; fetch transcript_url with your own key.
    WebhookBody:
      type: object
      properties:
        event:
          $ref: "#/components/schemas/WebhookEvent"
        delivered_at:
          type: string
          format: date-time
        test:
          type: boolean
          description: true only for POST /v1/destinations/{id}/test, whose run id is all zeros. Never create a record for it.
        run:
          $ref: "#/components/schemas/WebhookRun"
      required:
        - event
        - delivered_at
        - test
        - run
      description: 07 §12.3 — the delivery request body, signed as raw bytes (§12.4).
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              $ref: "#/components/schemas/ApiErrorCode"
            message:
              type: string
              description: A complete sentence written for a person. May be shown as-is.
            detail:
              type: object
              additionalProperties: true
              description: Always present; {} when there is nothing to add. Keys are per-code (07 §4.2).
            request_id:
              type: string
              description: Equals the X-Request-Id header. Quote it in a support conversation.
          required:
            - code
            - message
            - detail
            - request_id
      required:
        - error
      description: 02 §8.4 — the one failure envelope.
    RunListPage:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Run"
          description: segments is absent from every element, in every status.
        has_more:
          type: boolean
          description: Authoritative. Loop on this, not on whether data is empty.
        next_cursor:
          type:
            - string
            - "null"
          description: Opaque. null when has_more is false.
      required:
        - data
        - has_more
        - next_cursor
      description: 07 §6.1.
    LastDelivery:
      type: object
      properties:
        event:
          $ref: "#/components/schemas/WebhookEvent"
        status:
          $ref: "#/components/schemas/DeliveryStatus"
        status_code:
          type:
            - integer
            - "null"
        attempt:
          type: integer
        delivered_at:
          type:
            - string
            - "null"
          format: date-time
      required:
        - event
        - status
        - status_code
        - attempt
        - delivered_at
    Destination:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        url:
          type: string
          format: uri
          description: https:// only.
        events:
          type: array
          items:
            $ref: "#/components/schemas/WebhookEvent"
          minItems: 1
        enabled:
          type: boolean
        disabled_reason:
          type:
            - string
            - "null"
          description: Derived. A sentence when the destination was disabled automatically after five consecutive exhausted deliveries (07 §12.5); null otherwise.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        last_delivery:
          oneOf:
            - $ref: "#/components/schemas/LastDelivery"
            - type: "null"
      required:
        - id
        - name
        - url
        - events
        - enabled
        - disabled_reason
        - created_at
        - updated_at
        - last_delivery
      description: 07 §10.1. secret never appears here.
    DestinationWithSecret:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        url:
          type: string
          format: uri
          description: https:// only.
        events:
          type: array
          items:
            $ref: "#/components/schemas/WebhookEvent"
          minItems: 1
        enabled:
          type: boolean
        disabled_reason:
          type:
            - string
            - "null"
          description: Derived. A sentence when the destination was disabled automatically after five consecutive exhausted deliveries (07 §12.5); null otherwise.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        last_delivery:
          oneOf:
            - $ref: "#/components/schemas/LastDelivery"
            - type: "null"
        secret:
          type: string
          description: Shown ONCE — in this response and never again. Store it as SCRIPTRIP_WEBHOOK_SECRET on the receiver.
      required:
        - id
        - name
        - url
        - events
        - enabled
        - created_at
        - secret
      description: 07 §10.2, §10.4 — a destination whose secret was just created or rotated.
    DestinationListPage:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Destination"
        has_more:
          type: boolean
        next_cursor:
          type:
            - string
            - "null"
      required:
        - data
        - has_more
        - next_cursor
    Delivery:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Sent as X-ScriptRip-Delivery on every attempt of this delivery.
        run_id:
          type: string
          format: uuid
        event:
          $ref: "#/components/schemas/WebhookEvent"
        attempt:
          type: integer
          description: Attempts made so far. Incremented before each request is sent.
        status:
          $ref: "#/components/schemas/DeliveryStatus"
        status_code:
          type:
            - integer
            - "null"
          description: The receiver's last HTTP status; null when the request never completed.
        error:
          type:
            - string
            - "null"
          description: Why the last attempt failed, in words — including which SSRF check refused and what the name resolved to.
        next_attempt_at:
          type:
            - string
            - "null"
          format: date-time
          description: When the next attempt is due; null once terminal.
        delivered_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
      required:
        - id
        - run_id
        - event
        - attempt
        - status
        - status_code
        - error
        - next_attempt_at
        - delivered_at
        - created_at
      description: 07 §10.7. Every field is a deliveries column; destination_id is the path.
    DeliveryListPage:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Delivery"
        has_more:
          type: boolean
        next_cursor:
          type:
            - string
            - "null"
      required:
        - data
        - has_more
        - next_cursor
    TransferTarget:
      type: object
      properties:
        part_index:
          type: integer
          minimum: 1
        method:
          type: string
          const: PUT
        url:
          type: string
          format: uri
        headers:
          type: object
          additionalProperties:
            type: string
        expires_at:
          type: string
          format: date-time
      required:
        - method
        - url
        - headers
        - expires_at
      description: 06 §3.3–§3.4 — a presigned PUT. Content-Type is pinned to application/octet-stream.
    UploadOpened:
      type: object
      properties:
        upload_id:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - upload
            - recording
        status:
          $ref: "#/components/schemas/UploadStatus"
        storage_mode:
          $ref: "#/components/schemas/StorageMode"
        part_size:
          type:
            - integer
            - "null"
        max_parts:
          type: integer
        expires_at:
          type: string
          format: date-time
        run_id:
          type:
            - string
            - "null"
          format: uuid
          description: null when create_run was false.
        limits:
          type: object
          properties:
            max_run_bytes:
              type: integer
            max_run_seconds:
              type: integer
          required:
            - max_run_bytes
            - max_run_seconds
        target:
          oneOf:
            - $ref: "#/components/schemas/TransferTarget"
            - type: "null"
          description: "Present for storage_mode single: the one transfer target, inline."
      required:
        - upload_id
        - kind
        - status
        - storage_mode
        - max_parts
        - expires_at
        - run_id
        - limits
      description: 06 §3.3.
    UploadState:
      type: object
      properties:
        upload_id:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - upload
            - recording
        status:
          $ref: "#/components/schemas/UploadStatus"
        storage_mode:
          $ref: "#/components/schemas/StorageMode"
        part_count:
          type: integer
        received_bytes:
          type: integer
          format: int64
        received_part_indexes:
          type: array
          items:
            type: integer
          description: "The complete list, not a range: a hole at index 12 must be expressible."
        highest_contiguous_part:
          type: integer
        has_more_parts:
          type: boolean
          description: true only when the list was truncated at 10 000.
        run_id:
          type:
            - string
            - "null"
          format: uuid
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
      required:
        - upload_id
        - kind
        - status
        - storage_mode
        - part_count
        - received_bytes
        - received_part_indexes
        - highest_contiguous_part
        - run_id
        - created_at
        - expires_at
      description: 07 §9.3 — what a client asks after a crash.
    UploadProgress:
      type: object
      properties:
        upload_id:
          type: string
          format: uuid
        status:
          $ref: "#/components/schemas/UploadStatus"
        part_count:
          type: integer
        received_bytes:
          type: integer
          format: int64
        highest_contiguous_part:
          type: integer
      required:
        - upload_id
        - status
        - part_count
        - received_bytes
        - highest_contiguous_part
      description: 06 §3.4 — after a landed part is recorded.
    UploadTargets:
      type: object
      properties:
        upload_id:
          type: string
          format: uuid
        targets:
          type: array
          items:
            $ref: "#/components/schemas/TransferTarget"
      required:
        - upload_id
        - targets
      description: 06 §3.4 — transfer targets.
    UploadCompleted:
      type: object
      properties:
        upload_id:
          type: string
          format: uuid
        status:
          $ref: "#/components/schemas/UploadStatus"
        part_count:
          type: integer
        received_bytes:
          type: integer
          format: int64
        sha256:
          type:
            - string
            - "null"
          pattern: ^[0-9a-f]{64}$
        run:
          oneOf:
            - $ref: "#/components/schemas/Run"
            - type: "null"
        message:
          type: string
      required:
        - upload_id
        - status
        - part_count
        - received_bytes
        - sha256
        - run
        - message
      description: 07 §9.5.
    UploadAborted:
      type: object
      properties:
        upload_id:
          type: string
          format: uuid
        status:
          $ref: "#/components/schemas/UploadStatus"
        run:
          type:
            - object
            - "null"
          properties:
            id:
              type: string
              format: uuid
            status:
              $ref: "#/components/schemas/RunStatus"
          required:
            - id
            - status
        message:
          type: string
      required:
        - upload_id
        - status
        - run
        - message
      description: 07 §9.6.
  responses:
    InvalidRequest:
      description: 400 invalid_request — A malformed body; an unknown field; a missing required field; a value outside an enumeration; a source object that is not exactly one of the three shapes; a bad part_index; a missing etag in multipart mode; a sha256 that is not 64 lowercase hex; a format outside output_format; a malformed or expired cursor; a limit outside [1, 100].
      headers:
        X-Request-Id:
          description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
          schema:
            type: string
        Cache-Control:
          description: Always no-store on an authenticated response.
          schema:
            type: string
            const: no-store
        RateLimit-Limit:
          description: "07 §5.3: the refill rate per window."
          schema:
            type: integer
        RateLimit-Remaining:
          description: Tokens left in the bucket with fewer remaining.
          schema:
            type: integer
        RateLimit-Reset:
          description: Delta seconds until the bucket is full.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: invalid_request
              message: The field source.url is not a recognised YouTube URL shape.
              detail:
                field: source.url
                reason: is not a recognised YouTube URL shape
              request_id: req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55
    Unauthenticated:
      description: 401 unauthenticated — No Authorization header and no session cookie; a token that does not match a live key; a revoked key; an expired session.
      headers:
        X-Request-Id:
          description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
          schema:
            type: string
        Cache-Control:
          description: Always no-store on an authenticated response.
          schema:
            type: string
            const: no-store
        RateLimit-Limit:
          description: "07 §5.3: the refill rate per window."
          schema:
            type: integer
        RateLimit-Remaining:
          description: Tokens left in the bucket with fewer remaining.
          schema:
            type: integer
        RateLimit-Reset:
          description: Delta seconds until the bucket is full.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: unauthenticated
              message: This request carried no valid credential.
              detail: {}
              request_id: req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55
    Forbidden:
      description: 403 forbidden — The key lacks the route's scope; a cookie-authenticated state-changing request from a foreign Origin; a destination url that is not https:// or resolves into a private network.
      headers:
        X-Request-Id:
          description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
          schema:
            type: string
        Cache-Control:
          description: Always no-store on an authenticated response.
          schema:
            type: string
            const: no-store
        RateLimit-Limit:
          description: "07 §5.3: the refill rate per window."
          schema:
            type: integer
        RateLimit-Remaining:
          description: Tokens left in the bucket with fewer remaining.
          schema:
            type: integer
        RateLimit-Reset:
          description: Delta seconds until the bucket is full.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: forbidden
              message: This key does not have the runs:write scope.
              detail:
                required_scope: runs:write
                scopes:
                  - runs:read
              request_id: req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55
    NotFound:
      description: 404 not_found — An id that does not exist, or exists in another account. The two are indistinguishable by design.
      headers:
        X-Request-Id:
          description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
          schema:
            type: string
        Cache-Control:
          description: Always no-store on an authenticated response.
          schema:
            type: string
            const: no-store
        RateLimit-Limit:
          description: "07 §5.3: the refill rate per window."
          schema:
            type: integer
        RateLimit-Remaining:
          description: Tokens left in the bucket with fewer remaining.
          schema:
            type: integer
        RateLimit-Reset:
          description: Delta seconds until the bucket is full.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: not_found
              message: There is no run with that id in this account.
              detail:
                resource: run
                id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
              request_id: req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55
    Conflict:
      description: 409 conflict — Cancelling a run that is already terminal; a part reported against an upload that is not open; completing an upload whose parts do not reconcile; aborting a complete upload; requesting a transcript for a run that is not done; delivering a run that is not terminal or to a disabled destination.
      headers:
        X-Request-Id:
          description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
          schema:
            type: string
        Cache-Control:
          description: Always no-store on an authenticated response.
          schema:
            type: string
            const: no-store
        RateLimit-Limit:
          description: "07 §5.3: the refill rate per window."
          schema:
            type: integer
        RateLimit-Remaining:
          description: Tokens left in the bucket with fewer remaining.
          schema:
            type: integer
        RateLimit-Reset:
          description: Delta seconds until the bucket is full.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: conflict
              message: "The transcript is not ready: the run is still running."
              detail:
                status: running
              request_id: req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55
    PayloadTooLarge:
      description: 413 payload_too_large — declared_bytes above the account byte ceiling; received_bytes crossing it at a part report; a request body above 1 MiB.
      headers:
        X-Request-Id:
          description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
          schema:
            type: string
        Cache-Control:
          description: Always no-store on an authenticated response.
          schema:
            type: string
            const: no-store
        RateLimit-Limit:
          description: "07 §5.3: the refill rate per window."
          schema:
            type: integer
        RateLimit-Remaining:
          description: Tokens left in the bucket with fewer remaining.
          schema:
            type: integer
        RateLimit-Reset:
          description: Delta seconds until the bucket is full.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: payload_too_large
              message: This upload declares 3.2 GB and the ceiling for this account is 2.1 GB.
              detail:
                limit_bytes: 2147483648
                declared_bytes: 3221225472
              request_id: req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55
    UnsupportedMediaType:
      description: "415 unsupported_media_type — A request body without Content-Type: application/json; an Accept header that admits nothing this endpoint can produce."
      headers:
        X-Request-Id:
          description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
          schema:
            type: string
        Cache-Control:
          description: Always no-store on an authenticated response.
          schema:
            type: string
            const: no-store
        RateLimit-Limit:
          description: "07 §5.3: the refill rate per window."
          schema:
            type: integer
        RateLimit-Remaining:
          description: Tokens left in the bucket with fewer remaining.
          schema:
            type: integer
        RateLimit-Reset:
          description: Delta seconds until the bucket is full.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: unsupported_media_type
              message: Request bodies must be sent as application/json.
              detail:
                expected: application/json
              request_id: req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55
    UnprocessableSource:
      description: "422 unprocessable_source — A syntactically valid request whose source cannot be worked with: an unrecognised YouTube URL; an advisory duration above the ceiling; an upload_id that belongs to another account or is already consumed by a run."
      headers:
        X-Request-Id:
          description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
          schema:
            type: string
        Cache-Control:
          description: Always no-store on an authenticated response.
          schema:
            type: string
            const: no-store
        RateLimit-Limit:
          description: "07 §5.3: the refill rate per window."
          schema:
            type: integer
        RateLimit-Remaining:
          description: Tokens left in the bucket with fewer remaining.
          schema:
            type: integer
        RateLimit-Reset:
          description: Delta seconds until the bucket is full.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: unprocessable_source
              message: That is not a YouTube URL this service recognises.
              detail:
                reason: unrecognised_youtube_url
              request_id: req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55
    RateLimited:
      description: 429 rate_limited — The request rate ceiling for this key or account was exceeded. Nothing was consumed.
      headers:
        X-Request-Id:
          description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
          schema:
            type: string
        Cache-Control:
          description: Always no-store on an authenticated response.
          schema:
            type: string
            const: no-store
        RateLimit-Limit:
          description: "07 §5.3: the refill rate per window."
          schema:
            type: integer
        RateLimit-Remaining:
          description: Tokens left in the bucket with fewer remaining.
          schema:
            type: integer
        RateLimit-Reset:
          description: Delta seconds until the bucket is full.
          schema:
            type: integer
        Retry-After:
          description: "07 §4.3: delta seconds, never an HTTP-date."
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: rate_limited
              message: This key has sent more than 60 requests in the last minute.
              detail:
                scope: key
                limit: 60
                window_seconds: 60
              request_id: req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55
    QuotaExceeded:
      description: 429 quota_exceeded — The account's monthly seconds allowance is spent; the concurrent-open-upload ceiling is reached; the rolling daily byte ceiling is reached; more than 20 destinations.
      headers:
        X-Request-Id:
          description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
          schema:
            type: string
        Cache-Control:
          description: Always no-store on an authenticated response.
          schema:
            type: string
            const: no-store
        RateLimit-Limit:
          description: "07 §5.3: the refill rate per window."
          schema:
            type: integer
        RateLimit-Remaining:
          description: Tokens left in the bucket with fewer remaining.
          schema:
            type: integer
        RateLimit-Reset:
          description: Delta seconds until the bucket is full.
          schema:
            type: integer
        Retry-After:
          description: "07 §4.3: delta seconds, never an HTTP-date."
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: quota_exceeded
              message: This account has used its monthly transcription allowance.
              detail:
                limit_seconds: 36000
                used_seconds: 36012
              request_id: req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55
    InternalError:
      description: 500 internal_error — An unhandled exception. The message never carries it.
      headers:
        X-Request-Id:
          description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
          schema:
            type: string
        Cache-Control:
          description: Always no-store on an authenticated response.
          schema:
            type: string
            const: no-store
        RateLimit-Limit:
          description: "07 §5.3: the refill rate per window."
          schema:
            type: integer
        RateLimit-Remaining:
          description: Tokens left in the bucket with fewer remaining.
          schema:
            type: integer
        RateLimit-Reset:
          description: Delta seconds until the bucket is full.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: internal_error
              message: Something went wrong on our side. Quote the request id if it happens again.
              detail: {}
              request_id: req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55
    EngineUnavailable:
      description: 503 engine_unavailable — The Modal app cannot be reached and no live worker can claim; a storage provider will not issue a presigned URL; the operator has paused intake — in which case message is the operator's written reason, verbatim.
      headers:
        X-Request-Id:
          description: Echoed in an error envelope and written to the log line for this request. Quote it in a support conversation.
          schema:
            type: string
        Cache-Control:
          description: Always no-store on an authenticated response.
          schema:
            type: string
            const: no-store
        RateLimit-Limit:
          description: "07 §5.3: the refill rate per window."
          schema:
            type: integer
        RateLimit-Remaining:
          description: Tokens left in the bucket with fewer remaining.
          schema:
            type: integer
        RateLimit-Reset:
          description: Delta seconds until the bucket is full.
          schema:
            type: integer
        Retry-After:
          description: "07 §4.3: delta seconds, never an HTTP-date."
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: engine_unavailable
              message: No transcription capacity is available right now. Try again in a minute.
              detail:
                reason: no_capacity
              request_id: req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55
  parameters:
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
      description: Default 25, in [1, 100]. Outside the range is 400, never clamped.
    Cursor:
      name: cursor
      in: query
      schema:
        type: string
      description: Opaque. Pass it back unaltered.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      schema:
        type: string
        minLength: 1
        maxLength: 255
      description: A repeat within 24 hours returns the original response.
    RunId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    UploadId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    DestinationId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
webhooks:
  run.completed:
    post:
      summary: A run reached done.
      description: 07 §12. POSTed to every enabled destination subscribed to run.completed, signed. Answer 2xx within 10 seconds and do the work afterwards. segments is never in the body; fetch run.transcript_url with your own key.
      tags:
        - destinations
      parameters:
        - name: Content-Type
          in: header
          required: true
          schema:
            type: string
            const: application/json
        - name: User-Agent
          in: header
          required: true
          schema:
            type: string
            const: script-rip-webhooks/1
        - name: X-ScriptRip-Event
          in: header
          required: true
          schema:
            type: string
            const: run.completed
        - name: X-ScriptRip-Delivery
          in: header
          required: true
          description: The delivery id. Stable across every retry of the same delivery — a second de-duplication key beside (run_id, event).
          schema:
            type: string
            format: uuid
        - name: X-ScriptRip-Attempt
          in: header
          required: true
          description: 1 on the first attempt; the retry ladder is 1m, 5m, 30m, 2h, 12h, then exhausted.
          schema:
            type: integer
            minimum: 1
        - name: X-ScriptRip-Signature
          in: header
          required: true
          description: t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>"> keyed on the destination secret. Verify over the EXACT request bytes; reject |now - t| > 300 s.
          schema:
            type: string
            pattern: ^t=\d+,v1=[0-9a-f]{64}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookBody"
            example:
              event: run.completed
              delivered_at: 2026-03-11T18:09:59Z
              test: false
              run:
                id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
                source_type: youtube
                source_url: https://www.youtube.com/watch?v=dQw4w9WgXcQ
                title: The Long Way South — Part 4
                author_name: Sailing Somewhere
                author_url: https://www.youtube.com/@sailingsomewhere
                published_on: 2026-03-04
                duration_seconds: 2711.5
                thumbnail_url: https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg
                status: done
                lane: modal
                engine: whisperx large-v2
                language: en
                name_speakers: true
                segment_count: 412
                speaker_count: 2
                word_count: 6841
                speaker_labels:
                  SPEAKER_00:
                    name: Matt
                    role: host
                  SPEAKER_01:
                    name: Amy
                    role: guest
                cached_from_run_id: null
                error: null
                error_class: null
                retry_after: null
                created_at: 2026-03-11T18:04:11Z
                completed_at: 2026-03-11T18:09:57Z
                transcript_url: https://<host>/v1/runs/0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b/transcript
      responses:
        "410":
          description: "Gone: the delivery is exhausted immediately and never retried."
        2xx:
          description: Delivered. Any 2xx.
        default:
          description: "Any other status, a timeout (10 s), a TLS or DNS failure, or a 3xx (never followed): retried on the ladder."
  run.failed:
    post:
      summary: A run reached failed.
      description: "Same body shape with status, error, error_class and retry_after populated and transcript_url null. Branch on error_class: premiere means submit a NEW run after retry_after."
      tags:
        - destinations
      parameters:
        - name: Content-Type
          in: header
          required: true
          schema:
            type: string
            const: application/json
        - name: User-Agent
          in: header
          required: true
          schema:
            type: string
            const: script-rip-webhooks/1
        - name: X-ScriptRip-Event
          in: header
          required: true
          schema:
            type: string
            const: run.failed
        - name: X-ScriptRip-Delivery
          in: header
          required: true
          description: The delivery id. Stable across every retry of the same delivery — a second de-duplication key beside (run_id, event).
          schema:
            type: string
            format: uuid
        - name: X-ScriptRip-Attempt
          in: header
          required: true
          description: 1 on the first attempt; the retry ladder is 1m, 5m, 30m, 2h, 12h, then exhausted.
          schema:
            type: integer
            minimum: 1
        - name: X-ScriptRip-Signature
          in: header
          required: true
          description: t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>"> keyed on the destination secret. Verify over the EXACT request bytes; reject |now - t| > 300 s.
          schema:
            type: string
            pattern: ^t=\d+,v1=[0-9a-f]{64}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookBody"
            example:
              event: run.failed
              delivered_at: 2026-03-11T18:04:20Z
              test: false
              run:
                id: 0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b
                source_type: youtube
                source_url: https://www.youtube.com/watch?v=dQw4w9WgXcQ
                title: null
                author_name: null
                author_url: null
                published_on: null
                duration_seconds: null
                thumbnail_url: null
                status: failed
                lane: null
                engine: null
                language: null
                name_speakers: true
                segment_count: null
                speaker_count: null
                word_count: null
                speaker_labels: null
                cached_from_run_id: null
                error: This video is scheduled and has not aired yet.
                error_class: premiere
                retry_after: 2026-03-12T19:00:00Z
                created_at: 2026-03-11T18:04:11Z
                completed_at: 2026-03-11T18:04:19Z
                transcript_url: null
      responses:
        "410":
          description: Gone.
        2xx:
          description: Delivered.
        default:
          description: Retried on the ladder.
  run.canceled:
    post:
      summary: A person stopped the run.
      description: Same body shape; error_class is canceled. Also fires when a run in flight is deleted, before the row disappears.
      tags:
        - destinations
      parameters:
        - name: Content-Type
          in: header
          required: true
          schema:
            type: string
            const: application/json
        - name: User-Agent
          in: header
          required: true
          schema:
            type: string
            const: script-rip-webhooks/1
        - name: X-ScriptRip-Event
          in: header
          required: true
          schema:
            type: string
            const: run.canceled
        - name: X-ScriptRip-Delivery
          in: header
          required: true
          description: The delivery id. Stable across every retry of the same delivery — a second de-duplication key beside (run_id, event).
          schema:
            type: string
            format: uuid
        - name: X-ScriptRip-Attempt
          in: header
          required: true
          description: 1 on the first attempt; the retry ladder is 1m, 5m, 30m, 2h, 12h, then exhausted.
          schema:
            type: integer
            minimum: 1
        - name: X-ScriptRip-Signature
          in: header
          required: true
          description: t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>"> keyed on the destination secret. Verify over the EXACT request bytes; reject |now - t| > 300 s.
          schema:
            type: string
            pattern: ^t=\d+,v1=[0-9a-f]{64}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookBody"
      responses:
        "410":
          description: Gone.
        2xx:
          description: Delivered.
        default:
          description: Retried on the ladder.
