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

# Webhooks

> Receive signed event notifications and verify their authenticity

Webhooks let your backend receive event notifications instead of polling. You register an HTTPS
endpoint; Telepatia POSTs a signed JSON envelope to it whenever a subscribed event occurs.

Every delivery is signed with HMAC-SHA256 so you can prove it came from Telepatia. Register a
webhook with `POST /v1/webhooks`; the response returns a `signingSecret` **once** — store it now.

## Events

### Event catalog

| Event                      | Status | Fires when                                                |
| -------------------------- | ------ | --------------------------------------------------------- |
| `scribe_session.created`   | Live   | A new scribe session is created                           |
| `scribe_session.completed` | Live   | A scribe session finishes and its medical record is ready |
| `scribe_session.error`     | Live   | A scribe session fails to process                         |
| `scribe_session.updated`   | Live   | A finished session's record is edited afterward           |
| `scribe_session.cancelled` | Live   | A scribe session is discarded mid-recording               |
| `scribe_session.deleted`   | Live   | A scribe session is deleted                               |

Subscribe to one or more events per webhook (the `events` array at creation).

### Subscribing to events

Pass an `events` array when registering a webhook to choose exactly which events it receives. Each
delivery's `type` tells you which event fired, so a single endpoint can handle the whole lifecycle.

```bash theme={null}
curl -X POST https://scribe-api.telepatia.ai/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/scribe",
    "events": ["scribe_session.created", "scribe_session.completed", "scribe_session.error"]
  }'
```

Update the subscription at any time with `PATCH /v1/webhooks/{id}` and a new `events` array.

### Event envelope

Every webhook POST body is a JSON envelope. `id` is stable across redeliveries — dedupe on it.
`data` carries the event's business object — for `scribe_session.*` events, a snapshot of the
session:

```json theme={null}
{
  "id": "evt_01HXAMPLE",
  "type": "scribe_session.completed",
  "createdAt": "2026-06-23T12:00:00Z",
  "data": {
    "scribeSessionId": "ss_01HXAMPLE",
    "medicalRecordConfigurationId": "mrc_01HXAMPLE",
    "scribeSessionConfigurationId": null,
    "status": "completed",
    "institutionId": "inst_01HXAMPLE",
    "account": {
      "id": "acc_01HXAMPLE",
      "name": "Dr. Juan Salazar",
      "email": "dr.salazar@clinica.com"
    },
    "patient": {
      "id": "pat_01HXAMPLE",
      "name": "María Pérez",
      "idCountry": "COLOMBIA",
      "idType": "CC",
      "idValue": "1023456789"
    },
    "createdAt": "2026-06-23T11:40:00Z",
    "completedAt": "2026-06-23T12:00:00Z"
  }
}
```

The `data` object for `scribe_session.*` events:

| Field                          | Type           | Description                                                                                                                                     |
| ------------------------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `scribeSessionId`              | string         | Public id of the scribe session (`ss_…`).                                                                                                       |
| `medicalRecordConfigurationId` | string \| null | Public id of the medical record configuration used (`mrc_…`). Mutually exclusive with `scribeSessionConfigurationId`.                           |
| `scribeSessionConfigurationId` | string \| null | Public id of the scribe session configuration used (`ssc_…`). Mutually exclusive with `medicalRecordConfigurationId`.                           |
| `status`                       | string         | Final session status — `completed`, `completedWithErrors`, `error`, `cancelled`, or `deleted`.                                                  |
| `institutionId`                | string         | Institution that owns the session.                                                                                                              |
| `account`                      | object \| null | Treating professional — `id` (`acc_…`), `name`, `email`.                                                                                        |
| `patient`                      | object \| null | Patient — `id`, `name`, and identity `idCountry` / `idType` / `idValue` (same fields as the set-consultation-context request, for correlation). |
| `createdAt`                    | string         | When the session was created (ISO 8601).                                                                                                        |
| `completedAt`                  | string \| null | When the session finished (ISO 8601); `null` for error events.                                                                                  |

The same `data` shape is sent for every `scribe_session.*` event; `status` reflects the actual
session state and `completedAt` is `null` when the session did not complete.

## Verifying the signature

Each request carries an `X-Scribe-Api-Signature` header (Stripe-style):

```
X-Scribe-Api-Signature: t=1750000000,v1=5257a869e7ec...
```

`t` is the Unix timestamp the request was signed at; `v1` is the hex HMAC-SHA256 of
`"{t}." + raw_request_body`, keyed by your signing secret. Verify over the **raw** request body
before parsing — re-serializing JSON can change the bytes and break the comparison. Use a
constant-time compare and reject timestamps outside a 5-minute window to blunt replay attacks.

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

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

Each request also carries `X-Scribe-Api-Event-Id` (equal to the envelope `id`) for quick logging.

## Retry & delivery

* Return a `2xx` quickly to acknowledge. Do slow work asynchronously.
* A `5xx`, network error, or timeout is retried with exponential backoff (≈2s up to ≈2min between
  attempts) for up to 6 attempts across \~15 minutes.
* Any `4xx` (including `408` and `429`) is treated as permanent — the delivery is **not** retried.
* Retries reuse the same envelope `id`, so dedupe on it to stay idempotent.

## Rotating the signing secret

Call `POST /v1/webhooks/{id}/rotate-secret` to generate a new secret. The response returns the new
`signingSecret` once and bumps `signingSecretVersion`. The previous secret stops verifying
immediately, so roll the new value out to your endpoint before rotating.
