---
title: "Rehearse an apply plan on a contained branch"
method: POST
path: "/apply/v1/plans/{plan_id}/rehearse"
tags: ["Apply"]
---

# Rehearse an apply plan on a contained branch

`POST /apply/v1/plans/{plan_id}/rehearse`

## Request

- Base URL: `https://api.run402.com`
- URL: `https://api.run402.com/apply/v1/plans/{plan_id}/rehearse`
- Auth: one of:
  - API key in header `SIGN-IN-WITH-X`
  - HTTP bearer
  - HTTP bearer

Upgrades the source plan to reviewed_plan mode, rejects stale fingerprints before allocating a branch, snapshots the source project, creates a contained data branch, applies the candidate, and returns a dated rehearsal certificate. Passing reports include an exact bound commit call with required_plan; commits re-derive current facts and fail closed on drift. CI sessions are rejected in v1. The plan's bytes must already be uploaded: an unmet `missing_content` is refused 409 `REHEARSAL_CONTENT_MISSING` (with `details.missing_content` and an `upload_content` next action) before any snapshot or branch is spent; a project with no live release rehearses on an empty branch. The rehearsal runs on the gateway, detached from the request: with `Prefer: respond-async` the call answers 202 at once with the operation (`report: null`, a `poll` next action, header `Preference-Applied: respond-async`) and the report appears as `rehearsal_report` on GET /apply/v1/operations/{operation_id} once the operation is ready or failed; without it the 202 carries the report. A rehearsal whose gateway task stops before it finishes ends failed with a retryable OPERATION_INTERRUPTED naming the branch it left (`details.branch_project_id`).

## Path parameters

- `plan_id` string, required

## Headers

- `Prefer` string
- `SIGN-IN-WITH-X` string, required

## Request body

- RehearsePlanRequest
  - `teardown` 'keep' | 'on_pass' | 'always': on_pass (default): purge a passed rehearsal's branch, keep a failed one 7 days. keep: always keep. always: always purge. A new rehearsal first discards the project's earlier failed rehearsal branches unless their TTL was renewed.

## Response `202`

With `Prefer: respond-async`: the rehearsal is running (RehearsePlanAccepted); poll the operation for its `rehearsal_report`. Without it: the rehearsal completed or failed with a report (RehearsePlanResponse). A failed report is still a successful HTTP response unless the orchestration itself failed.

- union
  - RehearsePlanResponse
    - `operation_id` string, required
    - `status` 'planning' | 'uploading' | 'committing' | 'staging' | 'gating' | 'migrating' | 'schema_settling' | 'activating' | 'activation_pending' | 'needs_repair' | 'ready' | 'failed' | 'rolled_back', required
    - `poll_url` string, required
    - `report` ApplyRehearsalReport, required
      - `kind` 'rehearsal_report', required
      - `status` 'passed' | 'failed', required
      - `operation_id` string, required
      - `source_project_id` string, required: The source project's Run402 project ID.
      - `plan_id` string, required
      - `plan_fingerprint` string, required: Pure reviewed-plan content fingerprint bound to this rehearsal. Re-planning unchanged facts reproduces it.
      - `base_release_id` string, nullable, required: Concrete live release observed at rehearsal start.
      - `data_as_of` string, date-time, nullable, required: Snapshot capture time for the branch data; null only when orchestration failed before snapshot capture.
      - `branch_project_id` string, nullable
      - `branch_url` string, uri, nullable
      - `branch_plan_id` string, nullable
      - `branch_operation_id` string, nullable
      - `snapshot_id` string, nullable
      - `started_at` string, date-time, required
      - `completed_at` string, date-time, required
      - `duration_ms` integer, required
      - `migrations` object[], required
        - `id` string, required
        - `status` 'passed' | 'failed' | 'skipped', required
        - `error` string
      - `checks` RehearsalCheckResult[], required
        - `name` string, required
        - `type` 'migration' | 'commit' | 'static' | 'function' | 'declared', required
        - `status` 'passed' | 'failed' | 'skipped', required
        - `target` string
        - `method` string
        - `expected_status` union
          - integer
          - integer[]
        - `actual_status` integer, nullable
        - `duration_ms` integer, required
        - `error` string
      - `teardown` object, required
        - `policy` 'keep' | 'on_pass' | 'always', required
        - `action` 'kept' | 'deleted' | 'delete_failed' | 'skipped', required
        - `error` string
      - `fidelity` object, required
        - `secrets` 'absent', required
        - `parent_secret_keys` string[], required: Parent secret key names only; values are never copied or returned.
        - `email_mode` 'sandbox' | 'off', required
        - `scheduled_functions` 'paused', required
      - `next_actions` NextAction[], required
        - `type` string, required: Advisory machine-readable action type such as retry, authenticate, submit_payment, fund_wallet, renew_tier, check_usage, resume_deploy, edit_request, edit_migration, contact_support, upgrade_client, poll (poll the project events feed — appended when the envelope carries a correlated_platform_incident), or hand_to_member (on a commit/promote response that just activated a public site: show the human urls.site and urls.console, relay Run402's offer to promote the build on @run402com, and ask yes or no — see `credited_as` below).
        - `method` string
        - `path` string
        - `auth` string
        - `why` string
        - `credited_as` string, nullable: `hand_to_member` only. The caller's own live presence name in the project's default room, else that room's most recently active live presence, else `null` — in which case `why` tells the agent that joining the room with a name (`run402 rooms join --name ...`) is how the promotion credits it.
        - `body` object: `hand_to_member` only. A ready-to-send `POST /feedback/v1` template: `{ project_id, message: "promote: yes", handle? }`.
      - `error` DeployErrorEnvelope
        - `code` string, required
        - `phase` string
        - `resource` string
        - `message` string, required
        - `retryable` boolean
        - `operation_id` string
        - `plan_id` string
        - `fix` object
        - `logs` string[]
  - RehearsePlanAccepted
    - `operation_id` string, required
    - `status` 'planning' | 'uploading' | 'committing' | 'staging' | 'gating' | 'migrating' | 'schema_settling' | 'activating' | 'activation_pending' | 'needs_repair' | 'ready' | 'failed' | 'rolled_back', required
    - `poll_url` string, required
    - `report` null, nullable, required: Always null: the report is the operation's `rehearsal_report` once it is ready or failed.
    - `next_actions` object[], required
      - `type` 'poll', required
      - `method` 'GET', required
      - `path` string, required
      - `why` string, required

## Other responses

- `400`: INVALID_REHEARSAL_OPTIONS or non-rehearseable plan.
- `403`: FORBIDDEN or REHEARSAL_CI_UNSUPPORTED.
- `404`: PLAN_NOT_FOUND.
- `409`: REHEARSAL_PLAN_STALE when current gateway facts no longer match an already-reviewed plan. The error includes the freshly computed fingerprint and re-plan next_actions; no snapshot or branch is created.

---

[API](https://skmtc.dev/run402/apis/run402-api.md) · [All operations](https://skmtc.dev/run402/apis/run402-api/llms.txt) · [OpenAPI document](https://skmtc.dev/run402/apis/run402-api/revisions/893607557f50?raw)
