Skip to main content

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
  }
}
  • type is encounter.completed or encounter.canceled for outcomes. A third event, encounter.message, fires during review when the physician messages your user — see below.
  • rx_count is the number of prescriptions issued — 0 means 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: true events 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_consentpending_identity in_reviewcompleted (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.