AnyBio

Care hand-offs API

The staff-only follow-up queue - list and detail, claim, contact attempt, resolve with a disposition, resume automation - with optimistic versioning and the messaging policy that routes it.

A care hand-off is a durable follow-up owned by a person. It is created when the loop cannot continue on its own for a patient, it moves queued to claimed to resolved, and while it stands, automated care SMS to that patient is withheld. The routes are under /api/v1/care-handoffs and are staff-session only: no API key, patient SDK token or patient account can claim ownership, record contact, or release a hold.

When a hand-off is created

A hand-off is opened with one of these reason values:

reasonWhat happened
message_budget_exhaustedThe patient's automated care SMS allowance for the window is spent
message_policy_missingNo patient messaging policy is configured for the environment, so no send is permitted
automation_pausedA protocol rule matched (a red flag on a reading or on inbound text) and automation is paused for the patient
delivery_outcome_unknownA send was attempted and the provider's outcome could not be established
budget_exhausted, policy_missingThe same two conditions, recorded by an older caller

There is at most one open hand-off per patient per environment. A second cause while one is open appends an automation_withheld event to the existing case, bumps its version so a person must refresh, and reopens it if it had been resolved with automation still paused. A new patient reply reopens a resolved case too; the prior disposition stays in the event log.

Each hand-off carries: state, reason, queue_label, routing_snapshot, assigned_to, claimed_at, due_at, escalation_due_at, escalated_at, resolved_at, resolved_by, disposition, resolution_note, automation_resumed_at, episode_id, version.

Read routes

Read scope on the key is not enough; the caller must be a signed-in staff member (admin, member or physician) of the organization. Responses carry Cache-Control: no-store.

GET /api/v1/care-handoffs

Query: status (queued, claimed, resolved), sort (created, the default, oldest first; or due, earliest first with unset last), xuser_id, assigned_to, project_id (membership by the project roster), overdue (true for unresolved cases past due_at), limit (1 to 100, default 50), offset.

Response: environment_id, items (the hand-off rows), total, and patient_names keyed by patient id.

GET /api/v1/care-handoffs/{id}

Query: events_before (a cursor for older events).

Response: environment_id, handoff, events (oldest first, up to 100, with next_event_cursor when more exist), patient (id, display_name), and recent_replies - the patient messages already attached to this case, never the patient's whole conversation.

Event types: automation_withheld, claimed, contact_attempt, resolved, automation_resumed, patient_reply_received, protocol_rule_routed, overdue_escalated. Each carries an evidence object of references (episode, session, message ids), never copies of patient text.

Write routes

Write scope on the key, plus a staff session. Every write takes expected_version, and every successful write increments version.

expected_version and the 409

The row's version is bumped by every state change, including ones a person did not make: fresh evidence, a new patient reply, an overdue escalation. A write whose expected_version does not equal the current version is refused:

HTTP 409 Conflict
{ "error": "Follow-up changed; refresh before acting" }

Re-read the detail, look at what changed, and resubmit with the new version. The same 409 is returned when the action is not valid in the current state (claiming a resolved case, contacting a case you do not own).

POST /api/v1/care-handoffs/{id}/claim

Body: { "expected_version": 3 }. Moves queued to claimed and sets assigned_to to the caller. Eligibility follows the routing snapshot: before escalation, the primary staff list; after escalation, primary plus backup. If none of the routed staff are still members, anyone on staff may claim, so a queue cannot be orphaned by a membership change. An organization admin may reclaim a claimed case.

POST /api/v1/care-handoffs/{id}/contact-attempt

Body: expected_version, outcome (contacted or no_answer), note (required, up to 4000 characters), next_due_at (required when outcome is no_answer; must be in the future and within a year). Only the assignee may record contact. A next_due_at moves due_at and, before escalation, escalation_due_at. Returns 422 with a message when the body is incomplete.

POST /api/v1/care-handoffs/{id}/resolve

Body: expected_version, disposition, note (required), resume_automation (boolean). Dispositions:

dispositionMeaning
follow_up_completedThe person reached the patient and the follow-up is done
no_outreach_neededReviewed; no contact was required
referredRouted onward to another team or service

Resolution alone does not release the automation hold. Only resume_automation: true here, or a later resume, sets automation_resumed_at; until then automated care SMS to the patient stays withheld and a new reply reopens the case. Resolving clears the attention contribution the hand-off raised.

POST /api/v1/care-handoffs/{id}/resume

Body: expected_version, note (required: why automation may resume). Valid only on a resolved case whose automation is still paused; the assignee or an admin may call it.

Routing, due times and escalation

Due and escalation times come from the environment's patient messaging policy at the moment the hand-off is created, frozen onto the row as routing_snapshot: due_at is now plus response_seconds; escalation_due_at is due_at plus escalation_seconds. A worker sweep (at most 100 cases per pass) marks unresolved cases past escalation_due_at as escalated, returns them to queued, clears the owner, and records overdue_escalated with "routing": "backup_queue". The sweep changes the in-product queue only; it sends no external notification and makes no claim of after-hours coverage.

The messaging policy routes

The hand-off queue and the per-patient message budget read one policy per organization and environment. There are no numeric defaults: a missing policy is itself a hand-off reason.

GET /api/v1/patient-messaging-policy

Read scope plus a staff session. Returns { "policy": ..., "environment_id": ... }; policy is null until one is saved.

PUT /api/v1/patient-messaging-policy

Admin scope plus an organization admin session. Body:

{
  "max_messages": 8,
  "window_seconds": 3600,
  "routing": {
    "queue_label": "Care team follow-up",
    "primary_user_ids": ["<staff uuid>"],
    "backup_user_ids": ["<staff uuid>"],
    "response_seconds": 600,
    "escalation_seconds": 120,
    "coverage_note": "Weekday clinic hours; overnight cases wait for the morning huddle"
  }
}

Validation: max_messages positive; window_seconds, response_seconds and escalation_seconds between 1 and 31,536,000; both staff lists non-empty, distinct, at most 100 each, and every id a current staff member; queue_label 1 to 120 characters; coverage_note 1 to 1000 characters. Unknown fields are rejected. Durations are elapsed time, not a staffed-hours calendar; coverage_note is where a team writes what its hours are.

GET /api/v1/patient-messaging-policy/staff

Read scope plus a staff session. Lists the staff members (id, name) who may appear in the routing lists.

Where it appears

The dashboard's Review Queue page carries the hand-off queue beside agent runs and held messages. The settings page for patient messaging edits the policy above.

On this page