Omi omi.projectz.xyz
API v1

Everything is a client of the same engine

The caregiver web app, the patient's phone, the pendant and this reference all talk to one service layer. A native app has nothing to reimplement: the run engine, the confirmation rules, the routing and the reports live on the server, and every state change is one JSON call.

Conventions
The shape a phone renders GET /api/v1/patient X-Patient-Token: … { "status": "success", "data": { "patient": { "id": 1, "name": "Eli", "voice_name": "Omi", "listen_seconds": 8, … }, "run": { "status": "running", "progress_label": "Step 3 of 6", "clock": "7:06 AM", "omi_line": "Time to brush your teeth.", "omi_hint": "Say \"Omi, done\" or press your button when you finish.", "listening": true, "listening_remaining_sec": 6, "pendant": "listening", "speech": { "event_id": 812, "text": "Time to brush your teeth.", "spoken": false, "audio_url": null, "voice": { "preset": "warm", "lang": "en-US", "gender": "female", "rate": 0.95, "pitch": 1.0, "device_voice": null, "driver": "browser" } }, "current_step": { "label": "Brush teeth", "status": "waiting", "waiting_sec": 95, "max_min": 8, "confirm_mode": "voice_or_button", "awaiting_second": false, … }, "steps": [ { "label": "Wake up", "status": "done", "elapsed_sec": 40, "cleared_by": "button", "repeats": 0, "ambient_acks": 0 }, … ], "on_duty": { "name": "Mom", "role": "primary", "window": { "label": "School morning", "start": "6:00 AM", "end": "8:00 AM" } } }, "pendant": { "name": "Home pendant", "status": "active", "battery_pct": 82 }, "next": [ … ] } }

Auth

X-API-Key for adults. A mobile app logs in once and keeps the key.
POST/api/v1/auth{email, password} → {api_key, user}
POST/api/v1/auth?action=signup{email, password, name, timezone?, sample_family?} → 201 {api_key, user, patient?}
GET/api/v1/authme + unread_notifications
PATCH/api/v1/auth{name?, phone?, timezone?}
POST/api/v1/auth?action=rotate | revokenew key / no key

Patients

Access is always a seat on the care team. Unknown or unowned ids are 404, never 403.
GET/api/v1/patientseveryone I care for, with my role
GET/api/v1/patients?id=Npatient + care team + on duty + devices + running_run_id
POST/api/v1/patients{name, timezone?, self_mode?} → 201 · {sample: true} seeds the sample family
PATCH/api/v1/patients?id=N{name?, timezone?, voice_name?, listen_seconds?} primary
DELETE/api/v1/patients?id=Narchive primary
POST/api/v1/patients?id=N&action=join-code{label?} → {join_code, expires_in_sec} primary
GET/api/v1/patients?id=N&phones=1phones bound to the patient primary
DELETE/api/v1/patients?id=N&phone=Trevoke a phone token primary

Care team + coverage

Who is on duty is decided by coverage windows; a gap falls to the primary.
GET/api/v1/care-team?patient_id=Nmembers, windows, on_duty
POST/api/v1/care-team{patient_id, email, role: caregiver|educator|backup, label?, can_push?, can_edit_own_window?, copy_all?, backup_after_min?, ends_on?} → 201 primary
PATCH/api/v1/care-team?id=Nsame optional fields primary
DELETE/api/v1/care-team?id=Nremove a seat primary
POST/api/v1/care-team?action=window{patient_id, member_id, label, weekday_mask, start_time, end_time} → 201 primary
DELETE/api/v1/care-team?action=window&id=N&patient_id=N primary

Routines

Shared library routines are read-only; using one copies it into the patient's own routines.
GET/api/v1/routines?patient_id=N[&category=mornings|mine][&q=]library with step counts
GET/api/v1/routines?patient_id=N&counts=1category counts
GET/api/v1/routines?id=N&patient_id=None routine with steps
POST/api/v1/routines{patient_id, name, category?, description?, steps?} → 201
POST/api/v1/routines?id=N&action=copy{patient_id} → 201 copy
PATCH/api/v1/routines?id=N&patient_id=N{name?, description?, category?}
DELETE/api/v1/routines?id=N&patient_id=Narchive
PUT/api/v1/routines?id=N&patient_id=N&steps=1{steps: [...]} replace all steps, in order
POST/api/v1/routines?id=N&patient_id=N&steps=1{label, prompt?, target_min?, max_min?, repeat_seconds?, confirm_mode?, …} append → 201
PATCH/api/v1/routines?id=N&patient_id=N&step=Supdate one step
POST/api/v1/routines?id=N&patient_id=N&step=S&action=move{delta: -1|1}
DELETE/api/v1/routines?id=N&patient_id=N&step=S

Schedules

Wall-clock in the patient's timezone. weekday_mask bit 0 = Monday. The server starts due runs every minute.
GET/api/v1/schedules?patient_id=Nall active
GET/api/v1/schedules?patient_id=N&week=YYYY-MM-DDthe week grid
POST/api/v1/schedules{patient_id, routine_id, start_time, weekday_mask?|on_date?, starts_on?, ends_on?, auto_start?} → 201
PATCH/api/v1/schedules?id=N&patient_id=N{…, status: active|paused}
DELETE/api/v1/schedules?id=N&patient_id=Narchive

Runs (the live moment)

One state object serves both views. Ticks are server-side; clients never keep time.
GET/api/v1/runs?patient_id=Nrunning run state + recent
GET/api/v1/runs?id=Nstate: steps with status, current_step, omi_line, omi_hint, listening, pendant, on_duty, clock
GET/api/v1/runs?id=N&events=1activity log, newest first
POST/api/v1/runs{patient_id, routine_id, demo?} start → 201
POST/api/v1/runs?id=N&action=pressthe button (phone token or caregiver)
POST/api/v1/runs?id=N&action=heard{text} — only addressed replies inside the listening window count
POST/api/v1/runs?id=N&action=heardmultipart 'audio' instead of {text} when the server speech driver is on (transcribed first)
POST/api/v1/runs?id=N&action=spoken{event_id} — the device finished saying speech.event_id; the listening window starts now
POST/api/v1/runs?id=N&action=more_time | repeat
POST/api/v1/runs?id=N&action=push | add_time | stopcaregiver: {minutes?}
POST/api/v1/runs?id=N&action=tickbring the state up to now
POST/api/v1/runs?id=N&action=elapse{seconds} demo runs only

Patient phone

X-Patient-Token. No password on the patient side, ever.
POST/api/v1/patient?action=claim{code, label?} → 201 {token, patient} (no auth)
GET/api/v1/patientpatient, run (state), pendant, next schedules
POST/api/v1/patient?action=press | heard | more_time | repeat | spokenact on the running routine (heard: {text} or multipart audio; spoken: {event_id})
POST/api/v1/patient?action=start{routine_id} self mode only

Devices (pendant)

X-Device-Token for the pendant; pairing needs the primary.
GET/api/v1/devices?patient_id=Npendants
POST/api/v1/devices{patient_id, name} begin pairing → 201 {pair_code}
POST/api/v1/devices?id=N&action=confirm{code} → {device_token} once
POST/api/v1/devices?id=N&action=activate | unpair | chime
PATCH/api/v1/devices?id=N{name?, volume?}
GET/api/v1/devices?me=1pendant: my status + run state
POST/api/v1/devices?action=buttonpendant: {press: single|long|double}
POST/api/v1/devices?action=heartbeatpendant: {battery_pct}

Speech

Every run state carries `speech`: {event_id, text, spoken, voice{preset, lang, gender, rate, pitch, device_voice, driver}, audio_url}. Driver "browser": the client speaks the text with that voice and listens itself. Driver "api": play audio_url, or upload audio to heard.
GET/api/v1/speech?event=Naudio/mpeg for one of Omi's lines (server driver; 501 otherwise) — any caller of that patient
GET/api/v1/speech?patient_id=Nthe voice this patient's clients should use
GET/api/v1/speech?voices=1preset catalogue + languages, for a settings screen
PATCH/api/v1/patients?id=N{voice_preset, voice_lang, voice_device?, voice_rate?, voice_pitch?} — set the voice primary

Notifications

Routed by coverage window. The mobile app polls; push can be layered on later.
GET/api/v1/notifications[?unread=1]newest first + unread count
POST/api/v1/notifications?id=N&action=read | ackack stops the 5-minute escalation
POST/api/v1/notifications?action=read_all

Reports

Per step: prompt-to-done, repeats, ambient replies, cleared by; plus suggestions that can be applied.
GET/api/v1/reports?patient_id=Nroutines that have run
GET/api/v1/reports?patient_id=N&routine_id=R[&week=YYYY-MM-DD][&demo=1]weekly report + suggestions
POST/api/v1/reports?patient_id=N&routine_id=R&action=apply{apply} from a suggestion primary
The speaking / listening loop a client implements
  1. Poll GET /api/v1/patient. When run.speech.event_id changes, say speech.text in speech.voice (or play audio_url).
  2. When speech finishes, POST ?action=spoken {event_id}. The server starts the listening window from that moment and returns the state with listening: true.
  3. Open the mic for listening_remaining_sec. Send what you heard as heard {text} (or upload audio). The server decides whether it counts — addressed reply, ambient acknowledgement, or outside the window — and answers with Omi's next line.
  4. The button (press) works at any time and needs none of this.
Not in this proof of conceptPush notifications (the rows exist; delivery is polling + email) and any external vault or identity integration. Server-side speech is built but off until a key is configured; today each device speaks and listens itself. The engine does not care which client presses the button.