Patient Impersonation
Clinic staff acting on a patient's behalf — assisted form fill, accessibility help, language barriers, technical support. The schema, middleware, and audit instrumentation ship in foundation 1B.13. This page is the feature-side reference.
Overview
Patient impersonation lets a clinic staff member open a time-bound, audited session in which they perform actions on behalf of a specific patient at their own clinic. Use cases:
- Elderly patient phones in to fill an intake form; receptionist fills it on their behalf while talking them through the questions.
- Patient with a vision impairment cannot navigate the booking UI; admin books an appointment for them.
- Patient speaks limited Romanian / English; bilingual support staff translates and acts in the system.
- Patient reports a UI bug; engineer reproduces the bug from the patient's view to debug.
Impersonation is clinic-internal — clinic staff acting on their own clinic's patients. It is distinct from the break-glass flow (1B.11), which governs platform staff accessing cross-tenant data.
Trust posture
- Time-bound — sessions expire (default 1 hour, max 4); cannot be silently extended
- Reason required — every session captures a free-text reason (min 10 chars) for compliance review
- Audited end-to-end — every read and write inside an active session writes an
audit_logrow withaction_context = 'impersonation'andimpersonation_idlinking to the session - Patient-visible — the patient sees every session in their access-history view, with full metadata
- Permission-gated —
patients.impersonate(granted by default to admin + customer_support; can be added to custom roles) - Rate-limited — max 3 active sessions per staff principal per 5 minutes
The audit trail attributes the staff member as actor_id (not the patient). The patient is identified through the session record. This matches the regulator's expectation: when looking back at "who did this action", the answer is the staff member who actually clicked the button.
API
Open a session
POST /v1/organizations/{org_id}/patient-impersonation-sessions
Authorization: Bearer <staff_token>
Content-Type: application/json
{
"patient_id": "33333333-3333-3333-3333-333333333333",
"reason": "Patient phoned in, requesting help completing the intake form",
"expires_in_minutes": 30
}Fields
| Field | Type | Required | Notes |
|---|---|---|---|
patient_id | UUID | yes | The patients.id row at this org |
reason | string | yes | Min 10 chars. Captured in audit; reviewable later |
expires_in_minutes | int | no | Default 60, max 240 |
Response: 201 Created
{
"data": {
"session": {
"id": "77777777-7777-7777-7777-777777777777",
"staff_principal_id": "88888888-8888-8888-8888-888888888888",
"target_patient_id": "33333333-3333-3333-3333-333333333333",
"organization_id": "9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"reason": "Patient phoned in, requesting help completing the intake form",
"opened_at": "2026-04-30T10:00:00Z",
"expires_at": "2026-04-30T10:30:00Z",
"closed_at": null
},
"session_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}The session_token is a short-lived JWT carrying the staff principal + session id. The staff client uses it as the Authorization: Bearer header for subsequent requests during the session; the auth middleware validates the session is open before handing the request through.
Errors
400 reason_required— reason shorter than 10 characters403 forbidden— caller lackspatients.impersonate404 patient_not_found— patient does not exist at this org429 rate_limited— 3 active sessions per 5 minutes per staff principal exceeded
Close a session
POST /v1/organizations/{org_id}/patient-impersonation-sessions/{session_id}/closeThe opening principal can always close. Other staff can close if they hold patients.manage. Sessions auto-close at expires_at.
List sessions (clinic admin)
GET /v1/organizations/{org_id}/patient-impersonation-sessions?staff_principal_id=&patient_id=&opened_after=&opened_before=Gated by patients.manage. Server-side paginated, sorted, filtered. DataTable consumer in 1C.2.
Patient view of own sessions
GET /v1/me/access-history?organization_id={org_id}Returns sessions where target_patient_id is one of the calling human's patient profile ids (resolved via current_human_patient_profile_ids()). Includes the linked audit-log entries via impersonation_id so the patient can see what entities were touched.
Acting inside a session
After opening, the staff client makes any request with Authorization: Bearer <session_token>. The middleware:
- Decodes the token, looks up the session, validates
opened_at < now < expires_atandclosed_at IS NULL. - Sets
PrincipalContext:principal_id= staff principal (used for audit + permissions)acting_as_patient_id= target patient id (used for RLS scope so the staff sees what the patient sees and writes appear to come from the patient at the data layer)impersonation_session_id= the session UUID
Routes that need impersonation use RequireImpersonation() — no scope argument, just a check that the request is running inside an active session.
Every audit row written during the request:
actor_id = <staff principal>
actor_type = 'human'
impersonation_id = <session id>
action_context = 'impersonation'Forms or appointments authored during the session have their data-layer authorship attributed to the patient (submitted_by_principal_id = patient's principal). The audit row is the forensic source of truth for "the staff member did this on the patient's behalf."
Patient transparency
Always-recorded:
- Every session appears in the patient's "Access history" page (1C.3) with: who opened it, when, the reason, how long it lasted, and the entities touched (linked via
impersonation_idfromaudit_log).
No real-time email notification in v1. The recorded history is the transparency mechanism. If a Romanian DPA review or a clinic procurement requirement asks for stronger notification, real-time email lands as a follow-up — at that point we'd add a per-patient setting toggle.
Use cases
Assisted form fill
# 1. Patient calls in
POST /v1/organizations/{org_id}/patient-impersonation-sessions
{
"patient_id": "...",
"reason": "Patient phoned, requesting help completing intake form for first appointment",
"expires_in_minutes": 30
}
# 2. Staff submits form using session_token
PATCH /v1/forms/{form_id}
Authorization: Bearer <session_token>
{ "values": { ... } }
# 3. Form is recorded as patient-authored at the data layer.
# audit_log row: actor_id = staff, impersonation_id = session,
# action_context = 'impersonation'.Accessibility booking
Patient with vision impairment:
POST /v1/organizations/{org_id}/patient-impersonation-sessions
{
"patient_id": "...",
"reason": "Vision-impaired patient, booking follow-up appointment over the phone"
}
# Staff books the appointment as if the patient were doing it themselves;
# audit captures who really did it.Verbal consent capture
When a patient gives verbal consent over the phone (e.g. an addition to their treatment plan):
POST /v1/organizations/{org_id}/patient-impersonation-sessions
{
"patient_id": "...",
"reason": "Patient verbally agreed to telemedicine consent during phone consult; recording on their behalf"
}
# Staff signs the telemedicine consent form via the session.
# Resulting consents row has source='form' + source_form_id;
# audit_log row attributes the action to the staff with impersonation_id set.
# The form record itself bears patient authorship at the data layer.This is the verbal-consent-via-impersonation path — distinct from source='staff_action' in the consents ledger (which is for staff flipping a SaaS-style toggle on a patient's behalf without a form). For Tier B medical consents that need a signed form, this is how staff capture the patient's verbal agreement when the patient cannot sign in person.
Best practices
For staff
- Always provide a clear reason — specific enough to explain in a compliance review six months later. The reason is free-text but reviewable.
- Minimise session duration: don't open
expires_in_minutes: 240for a 5-minute task. - Verify the patient's identity before opening (call-back number, security question, etc.) — the audit trail captures that you opened a session but not whether the person on the phone was actually the patient.
For development
- Routes that need impersonation gate with
RequireImpersonation(). The middleware does the session lookup; the handler trusts theacting_as_patient_idinPrincipalContext. - Never bypass audit logging during a session — it's the entire transparency mechanism.
For compliance
- Per-clinic monthly review of impersonation sessions (DataTable export).
- Flag patterns: high-frequency impersonators, sessions with vague reasons, sessions opened just before expiry.
- Anomaly investigation goes through standard staff-discipline channels — the audit data is the source of truth.
Foundation reference
The schema, middleware, audit instrumentation, and visibility model are owned by foundation 1B.13. This page is the feature-side reference; design changes happen in foundation.
- Foundation 1B.13 — Patient Impersonation Sessions
- Foundation 1B.11 — Platform Break-Glass (sibling pattern, platform-side)
- Foundation 1A.12 — Reserved Columns (impersonation_id)
- decisions.md → Why clinic is controller, platform is processor — the broader transparency posture
- How Patient Data Is Protected — business-facing overview
- Patient API Reference — surrounding endpoints