---
title: "The holder's page is open (site hold)"
method: POST
path: "/focus/{hold_id}/heartbeat"
tags: ["focus"]
---

# The holder's page is open (site hold)

`POST /focus/{hold_id}/heartbeat`

## 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}/heartbeat`
- Auth: HTTP bearer

Sent by the console about every 10 s while a site hold's story page is open (`heartbeat_s` on `GET /auth/me`). One single-row write: it sets `last_seen_at`, clears `left_at` and the new-alert deadline. Without it the hold stops being open after `open_s` and, if no clean leave was seen, returns its alarms after `idle_s` (the lost-heartbeat backstop). After a clean leave, a heartbeat while the hold is still valid (normally within 30 s, sooner if its idle or new-alert deadline comes first, later while a live call defers it) clears it and the hold carries on. A heartbeat after the hold's clocks ran out never revives it: 409 `hold_changed` with the released record. 409 `hold_changed` for a hold that is not the caller's. 404 while `SITE_HOLD_ENABLED` is off.

## Path parameters

- `hold_id` string, required

## 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/35a2a6ebedda?raw)
