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

# Webhooks

> Get told when a visit finishes, and verify the request came from RxNote.

> [!WARNING]
>
> **Coming soon.** Webhooks are not available yet — poll `GET /visits/{id}` until its `status` is `completed`,
> `no-speech` or `failed`. This page describes how they will work.

Instead of polling, add an endpoint under **Settings → API → Webhooks** (or the organization's API tab) and choose
its events. The signing secret is shown once.

## Events

| Event | Sent when |
| - | - |
| `visit.completed` | Everything requested is ready (also sent for `no-speech`). |
| `visit.failed` | The visit failed. |

Events usually arrive within a minute of the status change.

```json theme={null}
{
  "id": "evt_4Rt8Vb1Np3Zs7Hc2kQ9xLm",
  "type": "visit.completed",
  "created_at": "2026-10-21T10:42:00Z",
  "data": { "id": 90211, "patient_id": 4821, "status": "completed", "note": { "...": "..." } }
}
```

`data` is the same object `GET /visits/{id}` returns.

## Verify the signature

Every request carries:

```
X-RxNote-Signature: t=1792560000,v1=5d2c…
X-RxNote-Event-Id: evt_…
```

`v1` is the hex HMAC-SHA256 of `"<t>.<raw request body>"` with your endpoint's secret. Check it against the **raw**
body before parsing, compare in constant time, and reject a `t` more than five minutes old.

```javascript Node.js theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyRxNote(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const given = Buffer.from(parts.v1 ?? "");
  return given.length === expected.length && timingSafeEqual(Buffer.from(expected), given);
}
```

```python Python theme={null}
import hashlib, hmac, time

def verify_rxnote(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts.get("t", 0))) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

## Respond and retry

Respond with any `2xx` once you have stored the event. A timeout (10 seconds), `408`, `429` or `5xx` is retried after
1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours; other `4xx` answers are not retried. A retry carries the same
`X-RxNote-Event-Id` and body — use it to ignore duplicates. Failed deliveries are listed in settings, where you can
send them again.


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