# Arlo Health MCP Server - Agent Integration Guide

## What is Arlo Health?

Arlo Health is healthcare infrastructure for AI agents. It connects patients with licensed clinicians through asynchronous, agent-driven conversations. Clinicians can diagnose, treat, prescribe medications, order labs, and refer.

**Key capabilities:**
- Consultations with licensed clinicians
- Prescriptions for common conditions (sent directly to pharmacies)
- AI-powered triage that gathers symptoms before provider connection
- Pay-per-use (current pricing is returned by `get_payment_status`)
- Free price transparency: in-network cost estimates, provider network-status checks, and public-data evidence about a provider, from insurers' published rate files and CMS data (`search_care_prices`, `check_network_status`, `get_provider_evidence`)
- Care requests (US): real-world care actions — moving an external referral, booking, cancelling or rescheduling an appointment — that Arlo's care team executes after the patient approves on a hosted sheet (`create_care_request`, `get_care_request`, `list_care_requests`)

## The Conversation Model

A **conversation** is a continuous, never-closing thread between the patient and Arlo. Over its lifetime a single conversation can contain **multiple provider visits** ("consults"): a provider joins, provides care, then leaves, and the conversation keeps going. Users can have **many conversations open at once** — there is no single-active-conversation restriction.

Conversations are never terminally closed. A conversation's status is **phase-derived** from where it currently is in the flow:

| Status | Meaning |
|--------|---------|
| `IDLE` | No active care request. Re-engageable — message it to start triage again. |
| `TRIAGING` | AI triage is gathering information. |
| `PAYMENT_REQUIRED` | Triage complete; a payment gate must be cleared to connect a provider. |
| `MATCHING` | Payment cleared; waiting for a provider to join. |
| `WITH_PROVIDER` | A provider has joined and is actively providing care. |
| `EMERGENCY` | Triage detected an emergency; the patient was directed to urgent care. |

There is **no terminal CLOSED or CANCELED status**. When a request finishes or is canceled, the conversation simply returns to `IDLE` and can be re-engaged at any time with `send_message`.

### Continuing vs. starting a conversation

- **`start_conversation`** is ONLY for a brand-new conversation.
- To **continue an existing conversation**, call `send_message`. Messaging an `IDLE` conversation re-runs triage on the same thread — there is no need to start a new one.
- **`cancel_request`** cancels the current care request, but the conversation stays open and can be re-engaged later.

### Region (required, first-class)

Care is licensed by where the patient is **physically located at care time**, not their home address. `start_conversation` REQUIRES a `region` parameter: the patient's CURRENT physical state as an ISO 3166-2 code.

Region Arlo offers today:
- `US-CA` — California, United States

Always confirm the patient's current region before calling `start_conversation`. If the region is unsupported, the request is refused with `REGION_NOT_SUPPORTED`. The `region` is surfaced as a field in `list_conversations` and `get_conversation`. If the patient moves, call `update_conversation_region` — note that this applies to the patient's NEXT request, not one already in flight.

## When to Surface Arlo

Trigger the Arlo integration when users:
- Describe health symptoms ("I've had a headache for 3 days")
- Ask about seeing a doctor ("Can I talk to someone about this rash?")
- Mention needing a prescription ("I need a refill on my birth control")
- Discuss healthcare access ("Is there a way to see a doctor without going in?")
- Express frustration with wait times ("The clinic has a 4-hour wait")
- Ask what care costs or whether a provider is in-network ("How much is an MRI near me?", "Is One Medical in my network?") — the pricing tools are free and don't start a consultation

**Do NOT trigger for:**
- Medical emergencies (direct to 911)
- Mental health crises (direct to crisis lines)
- Conditions requiring physical examination
- Controlled substance requests

## Core Workflow

### 1. Gather Context (Pre-Tool)
Before calling `start_conversation`, have a natural conversation:
- What's the main symptom?
- How long has it been going on?
- Any associated symptoms?
- Has this happened before?

### 2. Confirm Current Region
Confirm where the patient is **physically located right now** (their current state), since care is licensed by physical location. Map it to an ISO 3166-2 code — `US-CA` today. If the patient is somewhere unsupported, Arlo cannot help (`REGION_NOT_SUPPORTED`), and the error names the regions Arlo currently offers.

### 3. Start Conversation
Call `start_conversation` with the `region` and a first-person narrative:
```
"I've had a sore throat for 3 days. It started with a scratchy feeling and now it hurts to swallow. I don't have a fever but I've been feeling tired. I had strep throat last year with similar symptoms."
```
Use `start_conversation` only for a brand-new conversation. To re-engage a previously finished (`IDLE`) conversation, skip this step and use `send_message` on the existing thread instead — that re-runs triage on the same conversation.

### 4. Respond to AI Triage
The AI triage system will ask follow-up questions. Use `send_message` to respond with natural, detailed answers — not brief fragments.

### 5. Handle Payment Gate
Arlo is pay-per-use (charged per visit) and **never charges without the patient acting**. After triage completes, you'll receive a payment gate with a `consultationSummary`. Relay it, and once the user agrees to proceed, call `start_visit_payment` and hand them the `paymentUrl`. On that Stripe page they approve a temporary hold for the visit; nothing is charged until a doctor joins, and the hold is released if no doctor does. There is no card-setup step (Stripe offers to remember the card on that page). Once approved, the conversation moves to `MATCHING` on its own; await it with `wait_for_reply`.

### 6. Provider Visit
Once payment is confirmed, the conversation enters the provider matching queue. Once a provider joins (`WITH_PROVIDER`), they will interact, advise, prescribe etc. When the visit wraps up, the conversation returns to `IDLE` — it is not closed. The same conversation can host future provider visits, and can be re-engaged any time with `send_message`.

## Tool Categories

### Authentication & Onboarding
| Tool | Purpose | Read-Only |
|------|---------|-----------|
| `init_signup` | Start OAuth flow for new users | No |
| `check_account_status` | Check auth and webhook status | Yes |
| `get_user_profile` | Get account and patient info, plus a `healthRecords` block: whether insurance records are connected and how many records exist per category (counts only) | Yes |
| `update_patient_info` | Update name, DOB, home state, medical history | No |
| `complete_onboarding` | Finish onboarding once the required profile fields are saved. Terms were accepted at sign-in — never ask the user to agree again | No |
| `start_onboarding` | Open the interactive setup widget (profile, records, finish) — for accounts that haven't finished setup | No |

### Health Records (US insurance)
| Tool | Purpose | Read-Only |
|------|---------|-----------|
| `get_health_records` | Read connected coverage, conditions, medications, allergies, labs, claims | Yes |
| `start_flexpa_link` | Start a records connection — returns an `authorizationUrl` for the user to sign in to their insurer (works for already-onboarded users) | No |
| `complete_flexpa_import` | Poll with the `linkNonce` until the connection finishes (`imported` / `cancelled` / `failed`) | No |

Connecting records is optional — care works without it. When `get_health_records` reports `connected: false` and the user wants to connect, call `start_flexpa_link`, hand over the URL, then poll `complete_flexpa_import`. For users still in setup, let the `start_onboarding` widget drive it instead.

`get_user_profile` also carries a `healthRecords` block (connection + sync state + per-category record counts, no record contents), so you can tell whether records exist — and whether reading them is worthwhile — without calling `get_health_records`.

### Conversations
| Tool | Purpose | Read-Only |
|------|---------|-----------|
| `list_conversations` | List conversations (each with `region` and phase-derived status) | Yes |
| `start_conversation` | Create a brand-new conversation (REQUIRES `region`) | No |
| `get_conversation` | Get a conversation's status, `region`, and messages | Yes |
| `cancel_request` | Cancel the current care request (conversation stays open) | No |
| `update_conversation_region` | Update the patient's region for their NEXT request | No |
| `get_visit_notes` | Get provider clinical notes across all visits, grouped per visit | Yes |

### Messaging
| Tool | Purpose | Read-Only |
|------|---------|-----------|
| `send_message` | Send text/photo/video in a conversation (also re-engages an IDLE one); returns immediately — await the reply with `wait_for_reply` | No |
| `wait_for_reply` | Await the next reply (AI triage reply, provider connecting, or provider message); resumable — loop while it returns `stillWaiting` | Yes |
| `get_media_url` | Get signed URL for media attachments | Yes |

### Prescriptions
| Tool | Purpose | Read-Only |
|------|---------|-----------|
| `get_prescriptions` | List prescription history | Yes |
| `get_prescription` | Get prescription details | Yes |

Prescriptions are handled by Photon Health — the patient receives a text message (SMS) from Photon to choose a pharmacy and track their order; there is nothing to do in Arlo. The `fulfillment` field on prescription orders reads `PHOTON`.

### Care Pricing (price transparency, US)
| Tool | Purpose | Read-Only |
|------|---------|-----------|
| `search_care_prices` | Find nearby in-network care options with estimated negotiated prices under the patient's plan (~150 common outpatient services) | Yes |
| `check_network_status` | Check whether a specific provider/facility is in-network for the patient's plan, optionally with their contracted rate for a service | Yes |
| `get_provider_evidence` | Public-data evidence about ONE provider by NPI: Medicare-derived volume, facility outcome context, Medicare payment anchors, or an explained absence. Renders an evidence card on widget hosts | Yes |

All three tools are free — they never charge the patient and don't require an active care request. Estimates come from insurers' published machine-readable rate files and, for Original Medicare, Medicare's own fee schedules plus the CMS clinician enrollment file (currently Anthem Blue Cross CA/NY, Blue Shield of California, UnitedHealthcare incl. UMR/Oxford/Surest, Cigna, Aetna, HCSC, and Original Medicare; Medicare Advantage is declined with the reason), matched to the patient's plan via their linked insurance. Estimates are never guarantees — always relay the response's `disclaimer`, any `needsMoreInfo` asks (answering them upgrades range estimates to exact plan rates; set `planHints.unavailable=true` if the patient can't provide identifiers), and `rateAssessment`/`priceNote`/`note` caveats when present. `inNetwork: false` means "not found in the published files" — advise verifying with the provider, never state a hard "out of network".

Binding rules for these tools:
- **Named clinic/brand/provider ⇒ `check_network_status` first.** `search_care_prices` is area discovery ("what does this cost around here"), not a clinic directory — it doesn't resolve WHO BILLS, and the same brand can bill under different legal entities at 4x+ different rates by market.
- **Never quote a single number (or a median) while the billing entity is unresolved** — on a `partitioned` billingOutlook, a `multiEntity` row, or multiple `billingCandidates`, present the fork and the outlook's `ask`, then resolve which entity bills.
- **Always read and relay `service.appliesTo` / `service.priceNote`** — verify the code's selection fact (age band, screening vs diagnostic) against the actual patient, and relay pricing semantics (ACA $0 preventive, facility fee excludes the physician's separate bill) with every number.
- **An all-out-of-network name search is not an out-of-network verdict** — the entity that actually bills may not carry the consumer name; resolve the billing NPI (EOB) and re-call before concluding anything.
- **Evidence is never a ranking.** Relay each `signals[].display` sentence as written; never compress a percentile into "best doctor" language; `level: facility` signals describe the facility, not the clinician; absence of data is never negative evidence. Call `get_provider_evidence` only once the conversation is about ONE specific provider.

**Public HTTP version (no auth):** the same three lookups are served at `https://api.arlohealth.ai/api/pricing.v1` with no login or key, for bots and shared templates that cannot carry a connector. `GET /api/pricing.v1` is the index; `/guide.md` is the agent guide (what to get from the person, how each route works, how to read every field), `/openapi.json` the contract, `/catalog` and `/payers` the inputs, `/skill.md` a drop-in SKILL.md. Same response shapes as these tools. Anonymous calls take the insurer and plan from `planHints` (no linked records) and are rate limited; consults, records, and payments stay on this MCP server.

### Payment & Billing
| Tool | Purpose | Read-Only |
|------|---------|-----------|
| `get_payment_status` | Check payment status and per-visit price | Yes |
| `start_visit_payment` | Stripe link where the patient approves the hold for one visit (charged only when a doctor joins) | No |

### Care Requests (care grants, US)
| Tool | Purpose | Read-Only |
|------|---------|-----------|
| `create_care_request` | Propose a care action (`move_referral` for an EXTERNAL referral, `book_appointment`, `cancel_or_reschedule`) for one patient on the account; returns `status: "pending_approval"` plus an `approvalUrl` for the user | No |
| `get_care_request` | Status (`pending` → `approved` / `declined` / `expired` → `executed` / `failed`) plus the patient-visible audit trail; poll it after handing over the link | Yes |
| `list_care_requests` | The account's care requests, newest first | Yes |

The patient approves on Arlo's hosted sheet with their own verification on their device. The agent only ever learns the status, never an approval artifact, and must **never open the `approvalUrl` itself**. Links expire 15 minutes after creation. Wherever an action points at a provider's office, pass BOTH identity (`npi` from `search_care_prices` / `check_network_status`, or `providerName`) and location (`city` + `state` required; `facilityName` / `address` / `phone` when known) — providers work at several locations and the action is directed to one. Pass `context` (a short plain-language why, shown on the sheet as the agent's note) and `cost` (`{amountCents, description}`) only when the action carries a charge. Consent is per patient: pass `patientId` (from `get_user_profile`) for a dependent, omit it for the default patient. Records release, refills, claim disputes, and standing payment grants are not available yet — the server answers `ACTION_NOT_AVAILABLE` with the current list; do not offer them. No webhook event fires for approval outcomes: poll `get_care_request` every 15-30 seconds, or when the user says they've decided.

## Important Patterns

### First-Person Narratives
Always format health concerns as first-person narratives, not clinical summaries:

**Good:**
> "I've been having sharp pain in my lower right abdomen for about 6 hours. It started as a dull ache after lunch and has gotten worse. I feel nauseous and have no appetite. I'm 28 years old and this has never happened before."

**Bad:**
> "Abdominal pain, 6 hours, lower right quadrant, nausea"

### Payment Gate Handling
Arlo is pay-per-use and **never charges without the patient acting**: the patient approves each visit on a Stripe Checkout page. The payment gate response includes:
- `consultationSummary`: Markdown summary of what Arlo can help with
- `paymentType`: always "pay_per_use"

Once the user agrees, call `start_visit_payment` with the conversationId and give them the `paymentUrl`. They approve a temporary hold there (charged only when a doctor joins, released otherwise). When they finish, the conversation moves to `MATCHING` on its own; call `wait_for_reply` to await the provider. If the page expired or they cancelled, call `start_visit_payment` again for a fresh link. Never collect card details in chat.

### Retrying Safely (`idempotencyKey`)
`send_message`, `start_conversation`, and `start_visit_payment` accept an optional idempotency key. Pass one whenever the call comes from a loop, a scheduled job, a subagent, or anything else that may retry after a lost response: a duplicate `send_message` puts the same text in the patient's clinical record twice, and a duplicate `start_visit_payment` opens a second checkout session for the same visit.

There are two ways to supply it, and retrying is really the runtime's job rather than the model's:
- **Preferred — `_meta["arlo.health/idempotencyKey"]` on the tool call.** Your runtime sets it when it issues (and re-issues) the call, so the model never has to think about it. Hosted connector hosts are only offered this channel; the parameter is not advertised to them.
- **`idempotencyKey` argument.** Advertised on agent hosts for runtimes that can't inject `_meta`. `_meta` wins if both are present.

Use a fresh key per intended action (a UUID, or something like `<conversationId>:<your-turn-id>`) and reuse it only when retrying that same action. Replaying a key returns the original result — marked with `_meta["arlo.health/idempotentReplay"]` — instead of executing again, for 24 hours. If the original call is still running, the replay returns `status: "duplicate_in_flight"`; read the conversation with `get_conversation` rather than hammering the tool. Calls that failed are not cached, so a retry after an error genuinely re-runs.

### Canceling a Request
`cancel_request` cancels the conversation's current care request; the conversation itself stays open and returns to `IDLE`, so the user can keep chatting and re-engage later. Its behavior depends on the current status:
- **`PAYMENT_REQUIRED`** — dismisses the payment gate and returns the conversation to AI-only triage on the same thread (the user can keep chatting).
- **`MATCHING`** — the patient already confirmed, but no provider has joined yet: the request is pulled back, the per-visit hold is released (they are not charged), and the conversation returns to AI-only triage on the same thread.
- **`TRIAGING`** — cancels the in-flight request.
- **`WITH_PROVIDER`** — refused (let the provider wrap up).
- **`IDLE`** — refused (nothing to cancel).
- **`EMERGENCY`** — refused.

### Re-engaging a Conversation
A finished conversation sits at `IDLE`. To pick it back up, just call `send_message` on it — that re-runs triage on the same thread. Do not call `start_conversation` for an existing conversation; that is only for brand-new threads.

### Message Response Waiting
`send_message` and `start_conversation` return immediately (fast-ack) — they do NOT block for a reply. After sending, call `wait_for_reply` with the conversationId to await the response: the AI's triage reply, a provider connecting, or a provider's next message. It's a single resumable wait you can loop on — if it returns `stillWaiting`, call it again. During triage, Arlo's agent may need more information before we can a) determine if we can help and b) connect you to a provider.

## Service Limitations

Arlo **cannot** help with:
- Medical emergencies (call 911)
- Mental health crises
- Controlled substances (opioids, benzodiazepines, stimulants)
- Conditions requiring physical examination
- Pediatric patients under 1 year
- Patients physically located outside a supported region (currently `US-CA`)

Arlo **can** help with:
- Infections (UTI, sinus, skin, ear)
- Skin conditions (acne, eczema, rashes)
- Allergies and cold/flu symptoms
- Birth control and sexual health
- Prescription renewals (non-controlled)
- Travel health consultations
- Minor injuries and pain

## Authentication

The MCP server uses OAuth 2.1 with PKCE. Discovery endpoints:
- Protected Resource: `/.well-known/oauth-protected-resource`
- Authorization Server: `/.well-known/oauth-authorization-server`
- Server Card: `/.well-known/mcp.json`

For unauthenticated users, call `init_signup` to begin the OAuth flow. Pass `webhookUrl`, `webhookToken`, and `deliveryContext` to `init_signup` at this point to register for real-time push notifications — see the *Webhook Notifications* section below. This is the only opportunity to register a webhook without the user re-authenticating.

### Connecting a generic / self-hosted MCP client

Hosted connectors (Claude.ai, ChatGPT) auto-discover everything below. If you are wiring up a CLI agent or a custom MCP client and the connection fails at the auth step, this is the checklist:

1. **Always read the metadata first.** Fetch `/.well-known/oauth-authorization-server` and use the `authorization_endpoint` / `token_endpoint` it returns. Do **not** hardcode endpoint paths — a common SDK bug is to skip discovery and fall back to the RFC 8414 default root paths (`/authorize`, `/token`).
2. **If your SDK can't be made to read the metadata,** the server also serves the OAuth endpoints at those default root paths as aliases — `/authorize` and `/token` behave identically to `/oauth/authorize` and `/oauth/token`. So a non-compliant client still works, but reading the metadata is the supported path.
3. **Dynamic Client Registration is supported** at `/register` (RFC 7591) — no pre-registration needed; PKCE with `S256` is required, and `token_endpoint_auth_method` is `none` (public client).
4. **Your `redirect_uri` must be an endpoint your client is actually listening on** at the moment the user's browser is redirected back. This is the single most common self-hosted connection failure:
   - **Cloud/remote agents**: use a publicly reachable **HTTPS** callback URL. NEVER `localhost` — a localhost callback points at the *user's* device, where your agent isn't running, so the sign-in result can never reach you.
   - **Local clients** (CLI agents, MCP Inspector): a loopback `http://localhost:<port>/...` callback is fine, but your local HTTP listener must be running for the whole flow.
   - The server validates `redirect_uri` at `/oauth/authorize` and rejects malformed or insecure values with a `400` JSON error (`error`, `error_description`, `hint`). Loopback callbacks that turn out to be unreachable produce a guided error page for the user (with a manual-continue option) instead of completing silently — and the failure is logged on Arlo's side.
5. **The flow needs a browser.** Authorization is interactive (a phone-code login on the Arlo patient portal + a consent screen). A client running in a headless/no-TTY context must open the authorization URL in a real browser rather than expecting a device-code or in-terminal flow.
6. **After connecting, reload the tool list.** Some clients cache the tool list from before auth completed; if the Arlo tools don't appear, restart the session or trigger your client's MCP reload so the now-authenticated `tools/list` is re-fetched.

## Autonomous Agents (background tasks, loops, cron, webhooks)

If you are a personal/autonomous agent rather than a chat UI — you can run work
between user turns, schedule jobs, and receive HTTP callbacks — this is the shape
that fits Arlo's asynchronous care model:

1. **Connect over OAuth** (browser required for the interactive login + consent;
   see the generic-client checklist above). Use a public HTTPS `redirect_uri`.
2. **Register your wake channel:** call `register_webhook` with a public HTTPS URL
   and a secret you generate. Do this once, right after connecting — it is how
   you learn about a provider reply that lands hours later.
3. **Start or continue care:** `start_conversation` (new thread, `region`
   required) or `send_message` (continue / re-engage an existing one).
4. **While you are in a turn,** `wait_for_reply` blocks for up to **4 minutes per
   call** and is resumable — re-call it while it returns `stillWaiting`. (The ~55s
   window is a chat-UI constraint: Claude/ChatGPT abort a tool call around 60s.
   You aren't held to it, so you're served a longer one, and `maxWaitSeconds`
   raises it to 10 minutes when you can afford to sit on the call — e.g. riding
   out provider matching.) Each `stillWaiting` result reports `waitedSeconds`.
   To use the full 10-minute ceiling, send a `progressToken` with the call or
   declare the `logging` capability at `initialize` — Arlo ticks the response
   stream every 12s while it blocks, which also keeps the connection from being
   closed as idle. Without either, a single wait is capped at 300s.
5. **Between turns, sleep.** Don't hold a loop open or poll `get_conversation` on
   a timer; end the turn and let the webhook wake you.
6. **On wake,** the notification gives you a `consultationId` and no clinical
   content by design. Call `get_conversation` with that id to read what changed,
   then relay to your user and continue.

**Browser handoffs are expected and supported.** Anything that needs a screen is
returned to you as a URL to hand to the user — you don't need to reproduce it
in text:

| Flow | Tool | What you get |
|---|---|---|
| Finish account setup | `start_onboarding` | `onboardingUrl` into the patient portal (widget hosts get an inline card instead); poll `get_user_profile` until `onboardingComplete` |
| Approve a visit payment | `start_visit_payment` | Stripe `paymentUrl`; the conversation moves to `MATCHING` once approved (await it with `wait_for_reply`) |
| Connect insurance records | `start_flexpa_link` | `authorizationUrl`; poll `complete_flexpa_import` with the `linkNonce` |
| Approve a care action | `create_care_request` | `approvalUrl` to Arlo's hosted approval sheet (widget hosts get an approval card); poll `get_care_request` until `approved` / `declined` / `expired` |

**Photos:** you send them yourself with `send_message`'s `media` object — either
base64 bytes in `media.data`, or an HTTPS `media.url` (a public attachment URL
from your own channel) that Arlo downloads server-side. There is no Arlo panel
for your user to upload into, so don't tell them to look for one.

**Identify yourself.** Send a distinctive MCP `clientInfo.name` (and/or
User-Agent). Arlo tailors tool descriptions per host — an identified agent gets
the agent-shaped guidance and the webhook tools instead of connector guidance.

## Error Handling

Common error codes:
- `not_authenticated`: User needs to sign up/sign in
- `tool_retired`: `confirm_provider_connection` / `create_payment_setup` were replaced — call `start_visit_payment`
- `summary_loading`: the payment gate is still being set up — retry `start_visit_payment` shortly
- `no_payment_gate`: Conversation not in PAYMENT_REQUIRED status
- `region_not_supported`: The patient's current region is outside Arlo's licensed areas (currently `US-CA`)
- `onboardingIncomplete`: `complete_onboarding` was called before the required profile fields were saved — the payload lists them
- `unsupported_country` / `invalid_state`: `update_patient_info` received a non-US country, or a state code that is not a US state
- `ACTION_NOT_AVAILABLE`: `create_care_request` asked for an action type Arlo does not execute yet — the payload lists the available ones

## Webhook Notifications

**Webhooks are ONLY for agents that can expose a public HTTPS endpoint.** Hosted connector platforms (Claude.ai, ChatGPT, and similar) cannot receive webhooks — on those hosts, rely instead on `wait_for_reply` (the resumable await), `get_conversation` polling, and the live consultation widget.

For autonomous and self-hosted agents, Arlo pushes a notification when provider activity occurs. This is how you learn about care that progresses after your turn ends — strongly recommended, since a provider reply can take minutes to hours.

### Registering a Webhook

Two ways, depending on how you authenticated:

**Already authenticated over OAuth (the usual case for an autonomous agent):** call the `register_webhook` tool at any time — right after connecting is best. No `init_signup` needed; the webhook is stored against the authenticated account.

```json
{
  "webhookUrl": "https://your-agent-endpoint/hooks/arlo",
  "webhookToken": "a-secret-you-generate",
  "deliveryContext": { "to": "user-123", "channel": "sms" }
}
```

Re-register any time to move endpoints. Note the credential semantics: `webhookToken` is never carried over from a previous registration — omitting it CLEARS the stored token, so pass it every time you want Arlo to send an `Authorization` header. Check what's stored with `check_account_status`, which reports webhook registration state.

**Bot/session auth (OpenClaw-style):** pass webhook config when calling `init_signup` (or `POST /auth/init` for REST):

```json
{
  "sessionKey": "your-session-key",
  "webhookUrl": "https://your-agent-endpoint/hooks/wake",
  "webhookToken": "your-secret-token",
  "deliveryContext": {
    "to": "+15551234567",
    "channel": "whatsapp",
    "deliver": true
  },
  "conversationSessionKey": "your-platform-session-key"
}
```

- `webhookUrl` — **Must be a publicly accessible HTTPS URL.** Arlo's backend servers will POST to this URL when events occur. Private/internal URLs (localhost, private IPs, VPN-only addresses) will not work. Tailscale Funnel URLs (`.ts.net`) are supported if Funnel is enabled.
- `webhookToken` — Secret your agent uses to verify the request is from Arlo (sent as `Authorization: Bearer <token>`)
- `deliveryContext` — Where to deliver responses (passed through in every webhook payload)
- `conversationSessionKey` — Your platform's session identifier for threading notifications into the right conversation

Webhook registration is permanent until the session is revoked. Re-registering (via `register_webhook` or a new `auth/init`) updates the stored URL and token.

### Webhook Payload

All notifications are sent as `POST {webhookUrl}` with:

```http
Authorization: Bearer <webhookToken>
Content-Type: application/json
```

```json
{
  "message": "New activity in your Arlo account: a new message in your consultation. Use the Arlo get_conversation tool with consultationId \"abc123...\" to fetch the latest messages, then summarize what's new for me.",
  "event": "provider_message",
  "eventId": "7f9c1a2e-...",
  "timestamp": "2026-07-31T00:00:00.000Z",
  "consultationId": "abc123...",
  "mode": "now"
}
```

- `event` — machine-routable event category (see the table below). Route on this instead of parsing `message`. Treat unrecognized values as "something changed, go fetch".
- `status` — present on status-change events: the conversation's new status (e.g. `PAYMENT_REQUIRED`, `MATCHING`).
- `consultationId` — the wire field name; it carries the **conversation (chat) ID** the event relates to. Use it to fetch the right conversation directly via `get_conversation` instead of listing all conversations. Present when the event is tied to a specific conversation (message, status change, provider match); omitted for account-level events.
- `eventId` / `timestamp` — unique per delivery; useful for dedup on your side.

**Security note:** Notification payloads never include message content. Your agent should fetch the full context from the API after receiving a webhook. This prevents prompt injection from untrusted provider input.

### Notification Types

| `event` | Meaning | Suggested handling |
|---|---|---|
| `triage_reply` | AI triage replied in a conversation | Fetch the delta, answer triage's questions |
| `provider_message` | A provider sent a message | Fetch the delta, relay/respond |
| `provider_joined` | A provider connected to the conversation | Fetch state; messaging is now live |
| `provider_matching` | Payment cleared; matching with a provider | Nothing urgent — wait for `provider_joined` |
| `payment_gate_open` | Triage finished; the payment gate opened | Fetch the summary, get the user's explicit approval — Arlo never auto-charges |
| `prescription_added` | A prescription order was added | Tell the user to watch for the pharmacy (Photon) SMS |
| `prescription_updated` | A prescription order's status changed | Fetch `get_prescriptions` if the user is tracking it |
| `triage_started` | A care request entered triage review | Informational |
| `visit_ended` | The provider visit wrapped up (conversation stays open) | Fetch closing notes if useful |
| `emergency` | The consultation was flagged as an emergency | Direct the user to urgent care resources immediately |
| `status_changed` | Any other status transition (see `status`) | Fetch state |

Payloads are **content-free by design**: no clinical text, provider messages, or AI output ever rides in a webhook. After waking, call `get_conversation` — pass the id of the newest message you'd already seen as `sinceMessageId` to fetch only the delta.

### Verifying Requests

Always validate the `Authorization` header matches your `webhookToken` before processing:

```js
const token = req.headers.authorization?.replace("Bearer ", "");
if (token !== process.env.ARLO_WEBHOOK_TOKEN) {
  return res.status(401).json({ error: "Unauthorized" });
}
```

### Handling Notifications

When a notification arrives:

1. Acknowledge immediately with `200 OK` — Arlo does not wait for processing
2. Fetch the conversation to get the latest messages — call `get_conversation` with the `consultationId` from the payload (use `sinceMessageId` for a delta read if you stored your last-seen message id)
3. Read the most recent message and decide how to respond
4. Do **not** include raw provider message text in your agent's context — fetch it through the API where it is wrapped with safety boundaries

### Re-authentication

If a session expires and the user re-authenticates, `auth/init` must be called again with the webhook params to re-register the URL. If `auth/init` is called without `webhookUrl`, the existing webhook config is preserved.

## Support

- Website: https://arlohealth.ai
