---
title: "Save or replace one secret of a stored app login (sealed at once, never returned)."
method: PUT
path: "/story-projects/{projectId}/app-connections/{connectionId}/secrets/{kind}"
tags: ["Sources"]
---

# Save or replace one secret of a stored app login (sealed at once, never returned).

`PUT /story-projects/{projectId}/app-connections/{connectionId}/secrets/{kind}`

## Request

- Base URL: `https://docs.sleeperhit.studio/api/v1`
- URL: `https://docs.sleeperhit.studio/api/v1/story-projects/{projectId}/app-connections/{connectionId}/secrets/{kind}`
- Auth: HTTP bearer

`{ value }`: the secret, for a kind the connection's sign-in method or header uses (else 400, nothing stored). `totpSeed` takes the base32 key or its otpauth://totp/ link; `storageState` takes Playwright storage state JSON. Each secret is sealed the moment it is saved: the website and the Story API can save or replace one but never read it back, MCP and Slug never take one, and no read returns one (only which kinds are saved). Only the machines that record a capture can open it, to sign in to your app; a saved session's cookies for your app (never a password) may also go to Cloudflare's hosted browser, which only reads a light app's screens to learn its features. The answer is the connection's metadata (a username shown masked). A PUT, so no Idempotency-Key. Demo account only: use a demo or test account, never a real customer's account or production data. Free. A story project is refused with 409 `project_not_product` (nothing is stored or started): app connections are for product projects. Listing, reading and deleting a story project's existing connections still work.

## Request body

- AppConnectionSecretValue: One secret, sent once to be sealed. It is never returned by any read.
  - `value` string, required

## Response `200`

The connection, with the secret listed as saved (never its value).

- AppConnectionEnvelope
  - `connection` AppConnection, required
    - `id` string, required
    - `projectId` string, required
    - `label` string, nullable, required
    - `origin` string, required: The app's https origin: the only site a secret is typed into or a header sent to (with `extraOrigins`).
    - `extraOrigins` string[], required
    - `loginUrl` string, nullable, required
    - `loginMethod` 'form' | 'storageState' | 'provider' | 'none', required
    - `providerEndpoint` string, nullable, required: `provider` only: the app's sleeper.capture/1 API. The saved grant is sent there, in the Authorization header, and nowhere else.
    - `loggedInWhen` object[], nullable, required: Capture preconditions that hold once signed in.
    - `headerKind` 'none' | 'cloudflareAccess' | 'vercelBypass', required
    - `maskSelectors` string[], required
    - `usernameHint` string, nullable, required: The username masked when it was saved ("de•••@example.com").
    - `status` 'untested' | 'verified' | 'failed', required
    - `lastVerifiedAt` string, date-time, nullable, required
    - `lastUsedAt` string, date-time, nullable, required
    - `lastTestRequestedAt` string, date-time, nullable, required
    - `lastTest` AppConnectionTest, required: One test sign-in check, including checks that could not reach the app. `failed` means the check ended without a successful sign-in; it does not by itself mean the app rejected the login. `not_ready` means setup is incomplete; `unavailable`, `runner`, `runner_busy` and `recipe_unavailable` mean the check is unavailable. `credentials_refused` reports that the app refused the username and password. `needs_field` and `sso_only` stop before anything is typed: the sign-in asks for something the saved login does not have; `recipe_failed` means a sign-in step stopped even after one repair. Never a secret.
      - `id` string, required
      - `status` 'running' | 'passed' | 'failed', required: `running`, `passed` or `failed`. A test still running long after it was asked for reads as failed (`runner`).
      - `requestedAt` string, date-time, required
      - `finishedAt` string, date-time, nullable, required
      - `reason` 'not_https' | 'off_origin' | 'captcha' | 'form_not_found' | 'credentials_refused' | 'mfa_required' | 'totp_refused' | 'not_signed_in' | 'redeem_refused' | 'password_in_session' | 'timeout' | 'browser' | 'provider_refused' | 'unreachable' | 'not_ready' | 'unavailable' | 'runner' | 'runner_busy' | 'needs_field' | 'sso_only' | 'recipe_unavailable' | 'recipe_failed', nullable, required: Why it failed: given for an app whose origin is verified as yours, and for reasons on Sleeper's side (`not_ready`, `unavailable`, `runner`, `runner_busy`); null otherwise (a test is not a way to check logins for apps that are not yours). A running test can wait up to five minutes for a requested one-time code, entered only in that test on Connected apps. Story API, CLI, MCP and Slug never accept that code. `mfa_required` means that attempt ended without completing its second sign-in step. For an interactive retry, enter a requested code only in the running sign-in's temporary field. For unattended sign-in, save an authenticator setup key if the app accepts its codes, or use a saved session or capture grant. Sleeper cannot read email or texts, follow sign-in links, approve push requests, or use a passkey.
      - `message` string, nullable, required: What happened, in plain words.
      - `landedUrl` string, nullable, required: Where the browser was when it finished (no query or fragment); only for an app whose origin is verified as yours.
      - `hasScreenshot` boolean, required: Whether a picture of what the browser saw was kept. Only the owner's own reads link to it.
    - `retentionDays` integer, nullable, required
    - `secrets` object[], required: Which secrets are saved, and when. Never a value.
      - `kind` 'username' | 'password' | 'totpSeed' | 'workspace' | 'cfAccessClientId' | 'cfAccessClientSecret' | 'vercelBypass' | 'storageState' | 'providerGrant', required: One sealed secret of a stored app login. Reads name the kinds saved, never a value.
      - `savedAt` string, date-time, required
    - `missingSecrets` AppConnectionSecretKind[], required: The secrets its sign-in method and header still need, and a `workspace` when its sign-in steps ask for one.
    - `signInRecipe` AppConnectionSignInRecipe, required: THE SIGN-IN STEPS of a form sign-in (BL-2623): found by reading the signed-out app (no login is ever shown to what reads it), checked against the saved secrets before anything is typed, followed by every test, recording and walk, repaired once when a step no longer matches, and kept versioned. A secret is typed only into a field inside the form a step names. The sign-in page a person set wins (`loginUrlPinned`).
      - `version` integer, required
      - `source` 'scout' | 'repair', required
      - `foundAt` string, date-time, required
      - `lastPassedAt` string, date-time, nullable, required
      - `loginUrl` string, required: The sign-in page a run opens.
      - `loginUrlPinned` boolean, required: The person set the sign-in page.
      - `steps` object[], required
        - `fields` object[], required
          - `role` 'email' | 'username' | 'password' | 'workspace' | 'totp' | 'other', required
          - `label` string, required
      - `sso` object, required
        - `providers` string[], required
        - `only` boolean, required
      - `notes` string[], required
      - `summary` string, required: One sentence: "Signs in at /login · Email, then Password".
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required

## Other responses

- `400`: Request payload failed validation. The error message names the offending field.
- `401`: Missing, malformed, revoked, or expired API key.
- `403`: API key is valid but lacks the scope, is blocked by IP allowlist, or the account is disabled.
- `404`: No app connection with that id in this project (`app_connection_not_found`), or the project is not yours (`project_not_found`).
- `409`: `project_not_product` — the project is a story project: app connections are for product projects. PATCH its `kind` to `product` first; nothing was stored or started.
- `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.
- `503`: This server cannot seal a secret right now (`app_connection_sealing_unavailable`), so nothing was stored. Reads still work.

---

[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)
