Webhooks & Outcome Polling
Find out when a request reaches its final outcome — push (signed webhook) or pull (polling). Both return the same privacy-safe projection: status, prescription count, and your own identifier. Never the patient's identity or medical details.
1. Tag the request with your own ID
Pass external_id on the /prescribing/querycall — an opaque identifier from your system (an order ID, a row key). It's echoed back verbatim in the webhook and the polling endpoint so you can correlate the outcome without storing our encounter IDs.
curl -X POST https://api.appendix.com/api/v1/prescribing/query \
-H "Authorization: Bearer ak_live_..." \
-H "Content-Type: application/json" \
-d '{
"query": "34F, 3 days of dysuria ...",
"patient_email": "patient@example.com",
"patient_phone": "+14155550123",
"patient_state": "PA",
"patient_zip": "19103",
"external_id": "order_8842"
}'Never put PII in external_id — no names, emails, conditions, or anything derived from them. It leaves Appendix in webhook payloads; use an opaque key.
2. Configure your webhook
In the developer dashboard, set your endpoint URL under Outcome webhook. We mint a signing secret (whsec_…) you use to verify deliveries. HTTPS endpoints only.
When one of your requests reaches a terminal outcome, we POST:
POST <your webhook URL>
Content-Type: application/json
X-Appendix-Event: encounter.completed
X-Appendix-Signature: sha256=4f1c...
{
"id": "evt_7c2e...",
"type": "encounter.completed",
"created": "2026-06-10T18:02:11Z",
"sandbox": false,
"data": {
"encounter_id": "b1a2...",
"external_id": "order_8842",
"status": "completed",
"rx_count": 1
}
}typeisencounter.completedorencounter.canceledfor outcomes. A third event,encounter.message, fires during review when the physician messages your user — see below.rx_countis the number of prescriptions issued —0means the physician completed the request without prescribing (which can still be a complete review — advice, labs, or imaging). The review fee itself shows up in your credit ledger as a complete or incomplete review.sandbox: trueevents come from test keys (the consent-flow sandbox fires one so you can verify your handler end-to-end). Never treat them as real outcomes.- Respond with any 2xx within 10 seconds. We retry twice (after ~30s and ~2m) on failure, then give up — polling is the fallback.
- Every delivery's final result (success/failure, HTTP status, attempt count) is visible under Submissions & Webhooks in your dashboard for 30 days.
Physician messages: encounter.message
During review, the physician may need to ask your user a question. When that happens we email the patient a secure chat link directly, and we also send your webhook an encounter.message event so your app can surface the conversation in your own UI:
POST <your webhook URL>
Content-Type: application/json
X-Appendix-Event: encounter.message
X-Appendix-Signature: sha256=9a2b...
{
"id": "evt_3f8a...",
"type": "encounter.message",
"created": "2026-06-10T17:21:45Z",
"sandbox": false,
"data": {
"encounter_id": "b1a2...",
"external_id": "order_8842",
"status": "in_review",
"rx_count": 0,
"chat_url": "https://appendix.com/c/b1a2..."
}
}- The event is a doorbell, not the message. It tells you a physician message exists — never its content, author, or anything clinical. Physician chat is part of the Appendix↔patient care relationship.
chat_urlis the Appendix-hosted secure chat page for your user. Show it to them ("Your physician sent you a message — open secure chat"). The link carries no credential: the patient signs in with their Appendix account (created at consent) or uses the magic link from their email. Opening it yourself shows a sign-in wall, not the chat.- Your API keys are not accepted on the patient chat endpoints, and the prescribing API never returns a chat credential — by design, your systems cannot read the conversation.
3. Verify the signature
Every delivery is signed: X-Appendix-Signature = "sha256=" + hex(HMAC-SHA256(secret, raw body)). Verify before trusting the payload; rotate the secret from the dashboard if it ever leaks.
import crypto from "crypto";
function verifyAppendixSignature(rawBody, signatureHeader, secret) {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}4. Or poll
The same outcome is always available by polling — webhooks are the doorbell, polling is the source of truth. Filter by your external_id:
curl "https://api.appendix.com/api/v1/prescribing/encounters?external_id=order_8842" \
-H "Authorization: Bearer ak_live_..."
{
"encounters": [
{
"encounterId": "b1a2...",
"externalId": "order_8842",
"status": "completed",
"rxCount": 1,
"sandbox": false,
"createdAt": "2026-06-10T16:40:03Z",
"submittedAt": "2026-06-10T16:55:41Z",
"hasPhysicianMessage": true,
"lastMessageAt": "2026-06-10T17:21:45Z",
"chatUrl": "https://appendix.com/c/b1a2..."
}
]
}status values: awaiting_consent → pending_identity → in_review → completed (terminal), or canceled (terminal).
hasPhysicianMessage / lastMessageAt / chatUrl mirror the encounter.messagedoorbell for pollers: a physician message exists for your user, and here's the credential-free hosted chat page to send them to. Message content is never included.
What you will never receive
Appendix — not your app — holds the patient relationship. Webhook and polling responses never include the patient's verified identity, diagnosis, clinical decision detail, medication names, physician messages, or chat content. The patient is told exactly this in the consent modal: your app learns the outcome status, the prescription count, and whether a physician message is waiting — nothing more.