---
title: "Release the focus or the site hold"
method: POST
path: "/focus/{focus_id}/release"
tags: ["focus"]
---

# Release the focus or the site hold

`POST /focus/{focus_id}/release`

## Request

- Base URL: `https://api.verdun.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://api.verdun.ai/focus/{focus_id}/release`
- Auth: HTTP bearer

Idempotent: releasing the caller's own released focus is 200 with the record. From the commit every reservation of this focus is inert; the alarm markers are then cleared in chunks. A claim the focuser holds on one of those alarms is not touched. A `focus_id` that is not the caller's is 409 `focus_changed`. A generation of the caller's that is no longer inside their CURRENT scope (a revoked grant, a camera that moved) is still released, but the answer is 409 `focus_changed` with their in-scope current focus or null: its scope is never returned. For a site hold id the body may be `{"reason": "submit" | "flag_off"}` (default `submit`); the hold's markers are cleared, its alarms return to the shared queue with their original `queued_at`, and a hold that is not the caller's is 409 `hold_changed`.

## Path parameters

- `focus_id` string, required

## Request body

- ReleaseHoldRequest: Optional body of `POST /focus/{id}/release`. Only a site hold reads it; Focus ignores it.
  - `reason` 'submit' | 'flag_off': `submit` (the holder finished: Close all) or `flag_off` (the console's best-effort label when it stops on the capability going false). Only the line's reason differs.

## Response `200`

Successful Response

- union
  - FocusRecord: One Focus generation (`id` IS the generation: a restart is a new id).
    - `id` string, required: The focus generation id; a restart is a new id.
    - `site_id` string, required: The site the focus is on.
    - `scope` FocusScopeOut, required
      - `kind` 'site' | 'camera', required: Whether the focus covers the whole site or one camera.
      - `camera_id` string, nullable: Set iff `kind` is `camera`.
    - `operator_id` string, required: The focuser.
    - `operator_name` string, nullable: Their display name; null if the operator no longer exists.
    - `started_at` string, required: ISO 8601, UTC.
    - `last_input_at` string, required: Last accepted meaningful input or Keep (ISO 8601, UTC).
    - `check_due_at` string, required: `last_input_at` + FOCUS_CHECK_IDLE_SECONDS: when the console asks "Still focusing?".
    - `check_deadline` string, required: `check_due_at` + FOCUS_CHECK_ANSWER_SECONDS: with no explicit Keep by then the server releases the focus (and treats it as released at once, before any sweep).
    - `check_pending` boolean, required: True while the check is due and unanswered (active records only).
    - `check_due_in_seconds` integer, required: Seconds until `check_due_at`, computed server-side: time it from this against the local clock at receipt.
    - `check_deadline_in_seconds` integer, required: Seconds until `check_deadline`, computed server-side.
    - `state` 'active' | 'released', required: `active` while reserving alarms; `released` once it has ended.
    - `released_at` string, nullable: When the focus was released (ISO 8601, UTC); null while active.
    - `release_reason` 'manual' | 'check_timeout' | 'disabled' | 'scope_lost', nullable: Why it was released; null while active.
  - StoryHoldRecord: One site hold (`origin = "story"`, Site Story step 3): one operator owns the site while its story is open. `id` is the hold; `lineage_id` is shared by a Take over's successor, which is how the held alarms stay held for the taker with no gap.
    - `origin` 'story', required: Always `story`.
    - `id` string, required: The hold id (a site Take over creates a successor with a new id in the same lineage).
    - `lineage_id` string, required: Shared by a hold and its Take over successors.
    - `site_id` string, required: The site the hold is on.
    - `scope` FocusScopeOut, required
      - `kind` 'site' | 'camera', required: Whether the focus covers the whole site or one camera.
      - `camera_id` string, nullable: Set iff `kind` is `camera`.
    - `operator_id` string, required: The holder.
    - `operator_name` string, nullable: Their display name; null if the operator no longer exists.
    - `started_at` string, required: ISO 8601, UTC.
    - `last_seen_at` string, nullable: The holder's last heartbeat (ISO 8601, UTC).
    - `left_at` string, nullable: Set by the holder's best-effort leave signal, cleared by a heartbeat inside the grace. After a clean leave the hold returns its alarms about 30 s after this instant (a fixed server constant), or sooner on its idle or new-alert deadline.
    - `open` boolean, required: True while the holder's page is open: no clean leave and a heartbeat within `open_s`. While true there is no deadline of any kind.
    - `return_at` string, nullable: When the unattended hold returns its alarms to the queue (ISO 8601, UTC); null while `open`. The earliest of the idle deadline, the new-alert deadline and (after a clean leave) 30 s after `left_at`, pushed later by a live call. A display, never a decision.
    - `now` string, required: The server clock this record was built against: compute `return_at - now` once and tick it locally.
    - `serious_unopened` integer: Serious alarms the current holder has not opened (visibility only).
    - `oldest_serious_queued_at` string, nullable: When the oldest of those entered the queue (ISO 8601, UTC).
    - `open_seconds_total` integer: Seconds the holder's page was open during the hold (each interval since the last heartbeat, capped at `open_s`, settled at the next heartbeat, a leave or the release).
    - `idle_s` integer, required: Frozen at start: seconds since the last heartbeat before an unattended hold returns. The backstop for a page that vanished without a clean leave; after a clean leave the hold returns about 30 s later instead (a fixed server constant, not frozen).
    - `new_alert_s` integer, required: Frozen at start: seconds after a new alert before an unattended hold returns.
    - `open_s` integer, required: Frozen at start: seconds since the last heartbeat within which the page still counts as open.
    - `state` 'active' | 'released', required: `active` while it holds the site.
    - `released_at` string, nullable: ISO 8601, UTC; null while active.
    - `release_reason` 'idle_2min' | 'new_alert_2min' | 'holder_gone' | 'submit' | 'taken_over' | 'flag_off' | 'scope_lost' | 'disabled', nullable: Why it ended; null while active. `idle_2min` is the unattended return after a clean leave (about 30 s after it, or the idle deadline if that came first; the name is historical, not a measured 2 minutes); `holder_gone` is the idle deadline with no clean leave.

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/verdun/apis/verdun-api.md) - [All operations](https://skmtc.dev/verdun/apis/verdun-api/llms.txt) - [OpenAPI document](https://skmtc.dev/verdun/apis/verdun-api/revisions/35a2a6ebedda?raw)
