---
title: "Take over a site another operator holds"
method: POST
path: "/focus/{hold_id}/take-over"
tags: ["focus"]
---

# Take over a site another operator holds

`POST /focus/{hold_id}/take-over`

## 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/{hold_id}/take-over`
- Auth: HTTP bearer

Site-level Take over, one confirm. Body `{expected_operator_id, expected_focus_id?}` names the hold the console showed. If it is still that holder's valid hold, in one transaction it is released (`taken_over`) and the caller's hold is created IN THE SAME LINEAGE, so every held alarm stays held, now for the caller, with no gap; the displaced operator's live claims at the site then move to the caller. 200 with the caller's new hold record (also when the caller already holds it, which writes nothing). Otherwise nothing moves: 409 `claim_changed` with `hold` = the valid hold at the site now (or null). Open to every central-staff claim participant, owners included; a taker without `review_label_alarms` can act on the alarms but not label them. 403 out of scope or not central staff. 404 while `SITE_HOLD_ENABLED` is off.

## Path parameters

- `hold_id` string, required

## Request body

- TakeOverHoldRequest: Body of `POST /focus/{id}/take-over`: the hold the console showed. If it is no longer that holder's valid hold, nothing moves and the answer is 409 `claim_changed` with the current hold.
  - `expected_operator_id` string, uuid, required: The holder the console showed (`operator_id` of the record).
  - `expected_focus_id` string, uuid, nullable: The hold the console showed; must equal the path id when present.

## Response `200`

Successful Response

- 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/a19366be50a3?raw)
