> ## Documentation Index
> Fetch the complete documentation index at: https://docs.healos.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications when clinical notes and patient documents change state, instead of polling.

Healos can POST an event to a URL you control whenever a clinical note or a
patient document reaches a meaningful state. Register a webhook once and stop
polling the [notes](/clinical-notes) and document endpoints.

Every payload carries resource identifiers plus a `*_url` you can call — the
webhook is a notification, not the content itself. Fetch the content over the
authenticated External API using that URL.

<Note>
  `patient_id` is a BigInt serialized as a string. Treat it as an opaque string,
  same as elsewhere in the API. See [Making Requests](/making-requests).
</Note>

## Event types

| Event | Fires when | Pull the result from |
| - | - | - |
| `NOTE_CREATED` | A new clinical note is generated — a session is finalized, or a note is created via the API | `GET /notes/{noteId}` |
| `NOTE_REGENERATED` | A note is regenerated, for example via `POST /sessions/{sessionId}/notes/regenerate` | `GET /notes/{noteId}` |
| `NOTE_UPDATED` | An existing note is edited, saved, or reverted | `GET /notes/{noteId}` |
| `DOCUMENT_STATUS_CHANGED` | A patient document's text extraction reaches a terminal state (`completed` or `failed`) | `GET /patients/{patientId}/documents` |

## Payloads

Note events (`NOTE_CREATED`, `NOTE_REGENERATED`, `NOTE_UPDATED`) share one shape:

```json Note event theme={null}
{
  "event": "NOTE_REGENERATED",
  "note_id": "926f1540-2878-44b9-a2ba-9b5c4268d268",
  "session_id": "0be8b728-ffba-4dfb-8826-08fd850a8dc9",
  "patient_id": "1770623537795188951",
  "template_id": "11eaa736-ee98-4f71-b543-ec0ae62e5030",
  "source": null,
  "note_url": "https://api.healos.ai/ext-api/v1/notes/926f1540-2878-44b9-a2ba-9b5c4268d268",
  "timestamp": "2026-08-05T12:42:46.582Z"
}
```

`DOCUMENT_STATUS_CHANGED` reports the outcome of asynchronous text extraction:

```json Document event theme={null}
{
  "event": "DOCUMENT_STATUS_CHANGED",
  "document_id": "4822e568-ffc7-492a-b919-d69279f1329c",
  "patient_id": "1770623537795188951",
  "external_id": "your-emr-doc-id",
  "status": "completed",
  "error": null,
  "document_url": "https://api.healos.ai/ext-api/v1/patients/1770623537795188951/documents?external_id=your-emr-doc-id",
  "timestamp": "2026-08-05T12:49:24.118Z"
}
```

* `status` is `"completed"` or `"failed"`. On failure, `error` describes why.
* `document_url` is a direct document lookup when you set `external_id` at upload
  time, otherwise it points at the patient's document collection.

## Document upload flow

Text extraction runs asynchronously, so the upload response returns before the
text is ready. Use the webhook to learn when it finishes.

<Steps>
  <Step title="Upload the document">
    `POST /patients/{patientId}/documents`, optionally with an `external_id` form
    field. It returns `201` immediately. The document is typically `pending`,
    though it may already be `processing`, `completed`, or `failed` if extraction
    outran the response.
  </Step>

  <Step title="Wait for the event">
    When extraction finishes, Healos fires `DOCUMENT_STATUS_CHANGED`. No polling
    loop required.
  </Step>

  <Step title="Read the outcome">
    On `"completed"`, the document endpoints report `has_extracted_text: true`
    and `extracted_text_length`. The extracted text itself is not returned by the
    API — it is used internally as patient context.
  </Step>
</Steps>

<Warning>
  Rarely, if extraction could not be queued, the upload still returns `201` but
  its `message` omits "text extraction started" and no `DOCUMENT_STATUS_CHANGED`
  will fire. Call
  `POST /patients/{patientId}/documents/{documentId}/extract` to trigger it.
</Warning>

You can still poll `GET /patients/{patientId}/documents?external_id=<id>` if you
prefer, and re-trigger extraction with the `extract` endpoint above.

## Registering a webhook

Register and manage webhooks in the Healos dashboard at
[app.healos.ai/webhooks](https://app.healos.ai/webhooks).

<Steps>
  <Step title="Open the Webhooks page">
    Sign in and go to **[app.healos.ai/webhooks](https://app.healos.ai/webhooks)**.
  </Step>

  <Step title="Create a webhook">
    Select **Create webhook** and fill in:

    * **Name** — a label for the webhook.
    * **URL** — your HTTPS destination. Must resolve to a public host (see
      [Security](#security)).
    * **Event type** — one of the events above.
    * **Format** — `JSON` (default), `XML`, or `Form Data`.
    * **Headers** *(optional)* — static headers sent with every delivery, for
      example a shared secret.
  </Step>

  <Step title="Save">
    Save the webhook. Deliveries begin on the next matching event.
  </Step>
</Steps>

Webhooks you create are scoped to your account — you see and manage your own.

## Delivery semantics

These guarantees apply to the note and document events on this page.

* **Method** — `POST`, body encoded per the configured format, 30-second timeout.
* **Retries** — up to 3 attempts with exponential backoff (1s, then 2s, capped at
  5s). A `408`, `429`, or `5xx` response consumes a retry rather than counting as
  delivered.
* **Guarantees** — at-least-once. A failed or timed-out delivery is retried with
  the identical payload.
* **No redirects** — a `3xx` response is treated as an error, so a redirect can
  neither downgrade the delivery to HTTP nor re-route your payload.

<Warning>
  Deduplicate **retries** by the whole payload, or by the `event` + resource id +
  `timestamp` triple — not by resource id alone. The same `note_id` legitimately
  produces `NOTE_CREATED` and later `NOTE_REGENERATED` or `NOTE_UPDATED`, and the
  same `document_id` can emit `failed` and later `completed` after extraction is
  retried. Those are distinct events you must not collapse.
</Warning>

## Security

* **HTTPS and public hosts only.** Destinations are validated at registration and
  again immediately before every delivery. Loopback, private, CGNAT, link-local,
  and cloud-metadata ranges are rejected, IPv6 transition addresses included.
* **Identifiers, not content.** Payloads carry patient and resource identifiers,
  never clinical content. Fetch the content over the authenticated External API
  using the `*_url`.
* **Verify the source.** Send a secret through a configured header and check it on
  your side before trusting a delivery.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.