# Docly API

A facility can let another system **listen to its queue**. Docly pushes events; nothing outside can write. There is no endpoint that creates a token, calls a patient or changes a queue, and that is a standing position rather than a first-version limit.

**Webhooks are a convenience; the read API is the record.** A delivery to an unreachable endpoint is gone — not queued, not replayed — and the integrator reconciles from the read API instead.

## Built for agents

The specification is published at a stable address, [`developers.docly.in/openapi.json`](/openapi.json), with a machine-readable summary at [`/llms.txt`](/llms.txt) and this page as plain text at [`/index.md`](/index.md). Paste this into whatever assistant you use:

```text
Integrate Docly webhooks using the spec at developers.docly.in/openapi.json. Verify each delivery with HMAC-SHA256 over timestamp.body using the signing secret, and reject anything older than five minutes. Order events by sequence, never by arrival — retries mean deliveries can arrive out of order.
```

## Authentication

Setting up an integration returns **two secrets, shown once**: an API key the other system uses when it reads from Docly, and a signing secret it uses to verify that a delivery genuinely came from Docly. Two secrets because they do opposite jobs: the key travels on every request, the signing secret is never transmitted at all. Both are hashed at rest and shown exactly once.

The read API is versioned at `/v1`, because this is the one surface that cannot change under someone. Read-only: every endpoint is a `GET`.

```http
Authorization: Bearer dk_live_7f3a9c2b…
```

Either credential can be rotated on its own, with a grace period where the old and new both work, so deliveries in flight do not fail and a leaked key does not force the signing secret to change with it.

## Verifying a delivery

```http
POST https://their-endpoint.example.com/docly
X-Docly-Event:     token.called
X-Docly-Delivery:  dlv_8f3a…
X-Docly-Sequence:  4821
X-Docly-Signature: t=1696512000,v1=5d41402abc4b2a76b9719d911017c592
```

HMAC-SHA256 over `t.body` with the signing secret. **Reject anything where `t` is more than five minutes old**, or a captured delivery can be replayed later.

**No delivery ever carries a patient's name, number or age** — only the token's code and number. Nothing about money is deliverable at all.

## Ordering by sequence

**Order is not guaranteed.** Retries mean a delivery can arrive after a later one, so consumers order on `sequence`, never on arrival. Assuming otherwise is how a system shows a patient called before they were created.

Every event carries a sequence number and a timestamp. A snapshot carries the same `sequence`, so an integrator can tell whether a snapshot is newer than the last event it received.

When an endpoint is down, Docly retries with exponential backoff for about 24 hours, then dead-letters. A subscription that keeps failing pauses itself and emails the facility rather than retrying into silence.

## Rate limits

Each API key may make 120 requests a minute to the read API. A request over its limit is answered `429` with `Retry-After`.

## Errors

One shape everywhere. `field` is set only on validation failures.

```json
{ "code": "V007", "message": "Name is required", "field": "name" }
```

`404` means not found **or not yours** — the two are indistinguishable on purpose, so an id cannot be probed for existence.

## Endpoints

### GET /integrations

The facility's integrations, with the event catalogue

Administrators only. Neither secret is ever shown again: only its first characters.

Responses: `200`, `default`

### POST /integrations

Subscribe a system to the facility's events

Returns two secrets, shown once: an API key for reading from Docly, and a signing secret for verifying deliveries. Pro only (403 P202).

Responses: `201`, `default`

### PATCH /integrations/{id}

Change an integration's name, address or events

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | The integration, int_… |

Responses: `200`, `default`

### DELETE /integrations/{id}

Delete an integration: both credentials stop at once

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | The integration, int_… |

Responses: `204`, `default`

### GET /integrations/{id}/deliveries

Recent deliveries, newest first

Delivered rows are kept 7 days, failed ones 30.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | The integration, int_… |
| `state` | query | no | failed: only failed and dead-lettered deliveries |
| `cursor` | query | no |  |

Responses: `200`, `default`

### POST /integrations/{id}/deliveries/{delivery}/replay

Send a delivery again, now

The body is rendered from the event log at send time, with its original sequence.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | The integration, int_… |
| `delivery` | path | yes | The delivery, dlv_… |

Responses: `202`, `default`

### POST /integrations/{id}/resume

Resume a paused integration

An integration pauses itself when every delivery has failed for 6 hours, or 20 attempts in a row. What it missed is read back from the read API.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | The integration, int_… |

Responses: `204`, `default`

### POST /integrations/{id}/rotate

Rotate one credential; the old one keeps working for 24 hours

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | yes | The integration, int_… |

Responses: `200`, `default`

### GET /v1/queues

The facility's queues

Responses: `200`, `default`

### GET /v1/queues/{code}/snapshot

A queue's live state

`sequence` is the counter the webhooks carry, so an integrator can tell whether a snapshot is newer than the last event it received.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `code` | path | yes | The queue's code, e.g. p4k |

Responses: `200`, `default`

### GET /v1/reports/daily

The day's consolidated summary

`final: false` means a session was still running when this was computed; poll again after close.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `date` | query | no | YYYY-MM-DD; today when omitted |

Responses: `200`, `default`

### GET /v1/visits

Visits: the token-level record

Where anything a missed webhook would have carried is recovered. Oldest first; follow `next` until it is null. Up to 31 days a request.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `from` | query | no | YYYY-MM-DD |
| `to` | query | no | YYYY-MM-DD |
| `cursor` | query | no |  |

Responses: `200`, `default`

## Events

### day.closed

The day's summary, once its last session ends: the read API's daily report

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### facility.closed

The facility, a queue or a doctor closed for today

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### facility.reopened

The facility, a queue or a doctor reopened

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### practitioner.updated

A doctor was added, changed or removed

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### queue.updated

A queue was added, changed or removed

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### session.delayed

A session was delayed

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### session.ended

A session ended

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### session.opened

A session opened

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### session.paused

A session paused

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### session.resumed

A session resumed

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### token.called

A token was called

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### token.cancelled

A token was cancelled

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### token.checked_in

A patient checked in

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### token.completed

A token was completed

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### token.created

A token was created

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### token.no_show

A token was marked no-show, when its session ended

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### token.recalled

A skipped token was recalled

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### token.reordered

A token was moved in the queue

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

### token.skipped

A token was skipped

Headers: `X-Docly-Event`, `X-Docly-Delivery`, `X-Docly-Sequence`, `X-Docly-Signature: t=…,v1=…`. Verify the signature, then order by `sequence`, never by arrival.

The OpenAPI specification: [/openapi.json](/openapi.json).
