---
title: "Read one app capture."
method: GET
path: "/story-projects/{projectId}/captures/{captureId}"
tags: ["Sources"]
---

# Read one app capture.

`GET /story-projects/{projectId}/captures/{captureId}`

## Request

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

One capture: its status, what the check measured, the recorder's origin, route, app version and tour, and while `ready` a `playbackUrl` that works for an hour. Reads only.

## Response `200`

Capture envelope.

- AppCaptureEnvelope
  - `capture` AppCapture, required
    - `id` string, required
    - `projectId` string, required
    - `status` 'uploading' | 'ready' | 'rejected', required: `uploading` until finalize checks the file; then `ready`, or `rejected` with `rejectionReason`. An `uploading` capture whose verified file could not be saved stays `uploading` (retryable) and also carries `rejectionReason`.
    - `role` 'raw' | 'edit', required
    - `sourceCaptureId` string, nullable, required
    - `editMap` object, nullable, required
    - `hasTiming` boolean, required
    - `tourId` string, nullable, required: The stored tour (a Tour row) whose run this recording is, when the capture names one; null after the tour is deleted.
    - `featureId` string, nullable, required: The product feature (a Series Bible episode id) this capture shows. Kept when the Bible drops it.
    - `featureName` string, nullable, required: The feature's name in the Bible as it reads now; null when `featureId` is null or the Bible no longer lists it.
    - `featureSource` 'tour' | 'writer' | 'matched', nullable, required: How `featureId` was set: `tour` (copied from the tour it ran), `writer` (named at import, or chosen or confirmed by the writer) or `matched` (tagged by a strong match the writer has not confirmed yet). Null when it names no feature.
    - `suggestedFeature` AppCaptureFeatureSuggestion, required: The feature a capture most likely shows, matched from its name, page and tour against each feature's name, pages and tours.
      - `featureId` string, required
      - `featureName` string, required
      - `confidence` 'high' | 'medium' | 'low', required: `high` is a strong match (tagged by itself); `medium` a likely one (offered to the writer); `low` a weak guess.
      - `reason` string, required: In plain words: what in the recording names the feature.
    - `smoothness` AppCaptureSmoothness, required: How smoothly the recording machine drew the take. A take is only as smooth as the machine that recorded it: on a machine with no graphics card a page with heavy effects (blur, glass, blended gradients) is drawn only a few times a second, so the recording steps instead of moving while its sound plays on. Every take measures how fast its machine could draw the page (never how many new pictures a still page painted, which is legitimately few) and marks the stretches drawn slower than 24 fps as stuttering.
      - `verdict` 'smooth' | 'stutters', required: `stutters` when any stretch was drawn too slowly. Re-take it on a GPU runner: record the same tour on a machine with a graphics card (a Linux box with an NVIDIA GPU: `sleeperhit capture record <tour.json> --gpu nvidia`, or a Mac), import the new take, and play it instead.
      - `ownSoundLufs` number, nullable, required: How loud the take's own sound plays, LUFS, measured once when it was recorded and stored on the take beside its smoothness; null when nobody measured it (a hand upload, or a take recorded before the measurement), never the same as silence.
      - `paintedFps` number, required: New pictures painted a second over the take. Low on a still page, so never judged on its own.
      - `drawFps` number, nullable, required: How fast the recording machine could draw the page (requestAnimationFrame callbacks a second, median).
      - `slowestDrawFps` number, nullable, required: The slowest draw rate measured anywhere in the take.
      - `stutterSpans` object[], required: Where the picture stutters, in ms of THIS capture (an edit carries its raw master's through its cuts).
        - `startMs` number, required
        - `endMs` number, required
        - `drawFps` number, required
        - `paintedFps` number, required
      - `gpu` object, nullable, required: The recording machine's graphics as Chrome reported them.
        - `compositing` string, nullable, required
        - `renderer` string, nullable, required
        - `flags` string, nullable, required: `nvidia-egl` when the recorder launched Chrome on the NVIDIA GPU (--gpu nvidia); null for none.
    - `rejectionReason` string, nullable, required: Every way the file falls short of a capture, and the ffmpeg command that re-encodes it. Null unless the capture could not be saved: a `rejected` capture carries why, and an `uploading` one carries why its verified file could not be stored (still retryable).
    - `label` string, nullable, required
    - `fileName` string, nullable, required
    - `bytes` integer, required: The stored size once finalized; the declared size before.
    - `sha256` string, nullable, required: The stored file's SHA-256, once finalized.
    - `durationMs` integer, nullable, required
    - `fps` number, nullable, required
    - `width` integer, nullable, required
    - `height` integer, nullable, required
    - `hasAudio` boolean, nullable, required: The file carries sound you can hear: an audio stream that is not silence. Null until finalized.
    - `soundSpans` object[], nullable, required: Where the capture's sound is heard (louder than -45 LUFS), in ms on its own timeline: a take recorded with `sound: app` carries the app's own sound (a table read, a player). Null when it has no sound.
      - `startMs` integer, required
      - `endMs` integer, required
    - `originUrl` string, nullable, required
    - `route` string, nullable, required
    - `appVersion` string, nullable, required
    - `tour` object, nullable, required
    - `hasEvents` boolean, required: The recorder's event log was uploaded with it.
    - `checkpointCount` integer, required: How many per-step stills were uploaded with it.
    - `playbackUrl` string, uri, nullable, required: While `ready`: a link to watch it, until `playbackUrlExpiresAt` (read again for a new one).
    - `playbackUrlExpiresAt` string, date-time, nullable, required
    - `uploadExpiresAt` string, date-time, required
    - `createdAt` string, date-time, required
    - `finalizedAt` string, date-time, nullable, required

## Other responses

- `401`: Missing, malformed, revoked, or expired API key.
- `403`: API key is valid but lacks the scope, is blocked by IP allowlist, or the account is disabled.
- `404`: The named project or source does not exist or is owned by another account.
- `429`: Rate limit for this API key + IP + route family was exceeded.
- `500`: The API failed unexpectedly. Safe to retry with the same Idempotency-Key.

---

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