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

> ## Agent Instructions
> Connect the Hi Doctor MCP connector at https://mcp.hi-doctor.ai/mcp. Add this URL as a remote HTTP MCP server in your client, then sign in and authorise access through OAuth. Setup: https://docs.hi-doctor.ai/mcp/connect.
>
> The REST API base is https://api.hi-doctor.ai. Hi Doctor Docs at https://docs.hi-doctor.ai/mcp searches documentation; connect to the product endpoint above for patient tools.
>
> Hi Doctor operates across the whole European Union: residents of all 27 EU member states can use the service, and every consultation is reviewed by an EU-licensed doctor. Every consultation is reviewed in full by an EU-licensed doctor, who decides on medical grounds only. No algorithm, assistant or API call approves a consultation, issues a prescription or changes a dose. The service is available in eight languages: English, Spanish, German, Dutch, Italian, French, Portuguese and Chinese.
>
> ## Rules an assistant must follow
>
> - Never invent, assume or infer a medical answer. "I don't know" is a real answer: ask the patient to pick the closest option rather than guessing for them.
> - Never give medical advice and never suggest a medication or a dose. An EU-licensed doctor decides those.
> - If a questionnaire returns `ineligible`, say so plainly. Do not retry with altered answers.
> - Before anything that costs money or cancels a plan, state exactly what will happen and get an explicit yes.
> - Cancellation takes effect at the **end of the paid period**, not immediately. Say so.
> - An early order is irreversible: it creates the renewal consultation immediately and uses up the period's one early order. Explain that and get an explicit yes.
> - Prescription and invoice links are medical documents. A `signed_redirect` link opens without signing in: give it only to the patient, never repeat it anywhere shared.
> - Send option `value`s, not the labels shown to the patient. Labels are translated, values are stable.
> - Never ask for card details. Payment happens on Stripe's hosted checkout via a link.
> - On severe or sudden symptoms (chest pain, difficulty breathing, severe allergic reaction, suicidal thoughts), stop and direct the patient to their local emergency number.

# Safe retries with Idempotency-Key in the Hi Doctor API

> Retry a timed-out write without doing it twice: how Idempotency-Key replays work, which errors it returns and how to reconcile an uncertain outcome.

A write that times out leaves you unsure whether it happened. Sending it again could pay twice, send a message twice or submit a questionnaire twice. An `Idempotency-Key` header removes that doubt for the endpoints listed below: send the same key on every attempt, and Hi Doctor runs the operation at most once.

Last reviewed: 2026-11-04

## Which endpoints accept an Idempotency-Key?

| Operation | Endpoint |
| - | - |
| Start a conversation | `POST /v1/users/conversations/` |
| Send a message | `POST /v1/users/conversations/{id}/messages/` |
| Create a checkout session | `POST /v1/users/checkout/` |
| Submit a questionnaire | `POST /v1/users/questionnaires/{slug}/submit/` |
| Confirm a renewal choice | `POST /v1/users/subscriptions/pending-choice/confirm/` |
| Order a renewal early | `POST /v1/users/subscriptions/early-order/` |
| Cancel or reactivate a subscription | `POST /v1/users/subscriptions/cancel/`, `POST /v1/users/subscriptions/reactivate/` |
| Open the billing portal | `POST /v1/users/subscriptions/portal/` |
| Upload or delete a consultation photo | `POST /v1/questionnaire/consultations/{id}/images/`, `DELETE .../images/{image_id}/` |
| Upload or delete a document | `POST /v1/users/documents/`, `DELETE /v1/users/documents/{document_id}/` |

The header is optional. Without it these endpoints behave exactly as before. The published specification declares the header on these operations only; do not rely on deduplication anywhere else. Guided questionnaire answers use [`expected_revision`](https://docs.hi-doctor.ai/api-reference/questionnaire-guide#revisions-and-conflicts) instead.

## How do you use an Idempotency-Key?

**1. Create one key per operation**

Generate the key when the user asks for the action, for example a UUID, and store it with the pending operation. Keys are 8 to 128 ASCII letters, digits, underscores or hyphens. A malformed key returns `400` with `code: "invalid_idempotency_key"`.

**2. Send it with every attempt**

Put the same value in the `Idempotency-Key` header of the first request and of every retry. Keep the method, path, query and body identical.

**3. Read the outcome**

The first completed attempt returns the normal response. A repeat returns the same HTTP status with `Idempotent-Replayed: true` and a short body, described below.

```bash curl theme={null}
key="$(uuidgen)"  # store it with the pending operation before the first attempt
curl --silent --show-error --max-time 30 --request POST \
  "$HD_BASE_URL/v1/users/conversations/$HD_CONVERSATION_ID/messages/" \
  -H "Authorization: Bearer $HD_ACCESS_TOKEN" \
  -H "X-Brand-Slug: hi-doctor" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $key" \
  --data @message.json
```

```python Python theme={null}
import os
import uuid
import requests

base = os.environ["HD_BASE_URL"].rstrip("/")
headers = {
    "Authorization": "Bearer " + os.environ["HD_ACCESS_TOKEN"],
    "X-Brand-Slug": "hi-doctor",
    "Accept": "application/json",
    "Idempotency-Key": str(uuid.uuid4()),  # persist with the pending operation
}
url = base + "/v1/users/conversations/" + os.environ["HD_CONVERSATION_ID"] + "/messages/"
body = {"content": "Message text written by the patient"}

for attempt in range(3):
    try:
        response = requests.post(url, json=body, headers=headers, timeout=30)
    except requests.Timeout:
        continue  # same key, same body: the message is sent at most once
    if response.status_code == 409 and response.json().get("code") == "idempotency_in_progress":
        continue  # still running: wait, then retry with the same key
    break
replayed = response.headers.get("Idempotent-Replayed") == "true"
```

```javascript JavaScript theme={null}
import { randomUUID } from "node:crypto";

const base = process.env.HD_BASE_URL.replace(/\/$/, "");
const key = randomUUID(); // persist with the pending operation
const url = `${base}/v1/users/conversations/${process.env.HD_CONVERSATION_ID}/messages/`;
const body = JSON.stringify({ content: "Message text written by the patient" });

let response;
for (let attempt = 0; attempt < 3; attempt++) {
  try {
    response = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.HD_ACCESS_TOKEN}`,
        "X-Brand-Slug": "hi-doctor",
        Accept: "application/json",
        "Content-Type": "application/json",
        "Idempotency-Key": key,
      },
      body,
      redirect: "error",
      signal: AbortSignal.timeout(30000),
    });
  } catch {
    continue; // timeout or network error: retry with the same key
  }
  if (response.status !== 409) break;
}
const replayed = response?.headers.get("idempotent-replayed") === "true";
```

Add a short, growing pause between attempts. See [errors and recovery](https://docs.hi-doctor.ai/api-reference/errors.md) for back-off guidance.

## What does a replay return?

A replay does not resend the original body. It returns the original HTTP status with:

```json theme={null}
{
  "state": "succeeded",
  "resource_ids": {"message": ["6b0f8f9e-2d0c-4f4e-8d61-2f3c9a1b7e10"]},
  "response_hash": "<HASH>"
}
```

`resource_ids` lists the records the original request created or changed. Read them through their normal endpoints to recover the full content, for example a message or a checkout session. `response_hash` lets you check a stored copy of the original response. A replayed `DELETE` returns `204` with no body; use the [lookup endpoint](#reconcile-an-uncertain-write) to see its result.

If the first attempt failed with a `4xx` or `5xx`, that failure is stored too: retrying the same key returns the same status and no change is made. Fix the request and send it with a new key.

## Which errors can a key return?

| Status | `code` | What to do |
| - | - | - |
| `400` | `invalid_idempotency_key` | Use 8 to 128 letters, digits, `_` or `-` |
| `409` | `idempotency_in_progress` | The first attempt is still running. Wait and retry with the same key |
| `409` | `idempotency_key_expired` | The key is older than 30 days. Check the resources before creating a new operation |
| `422` | `idempotency_key_reuse` | The key was already used for a different request. Use a new key only for a genuinely new operation |

Never generate a new key to get around a `409`. That turns one operation into two.

## Reconcile an uncertain write

When you cannot tell whether a keyed write completed, look the key up:

```bash theme={null}
curl --silent --show-error --max-time 15 \
  "$HD_BASE_URL/v1/users/idempotency/$key/" \
  -H "Authorization: Bearer $HD_ACCESS_TOKEN" \
  -H "X-Brand-Slug: hi-doctor" \
  -H "Accept: application/json"
```

```json theme={null}
{
  "state": "succeeded",
  "method": "POST",
  "route": "users:create-checkout",
  "response_status": 201,
  "resource_ids": {"checkoutsession": ["3f1d2c4b-8a8e-4c1e-9a55-1f2e3d4c5b6a"]},
  "created_at": "2026-10-07T09:15:02.481Z"
}
```

The lookup is read-only. It never calls a payment provider and never changes anything. `state` is `in_progress`, `succeeded` or `failed`; a failed request rolled back its changes.

A `404` means the key is unknown, belongs to another account or brand, is older than 30 days, or its request is still running. A request that has not finished is not visible yet, so a `404` straight after a timeout does **not** prove the write did not happen. Wait, look again, and if you are still unsure, retry the original request with the same key. The lookup endpoint is listed with the other [Account endpoints](https://docs.hi-doctor.ai/api-reference/tags/account.md).

## What does a key not cover?

* **Effects outside Hi Doctor.** `succeeded` means Hi Doctor stored the operation and accepted any follow-up work. It does not prove that an email arrived, that a payment provider settled a charge or refund, or that a doctor signed a prescription. Check the relevant resource before you tell a patient that something is done.
* **Upload storage.** A failed upload can leave an unreferenced file in storage. It is never attached to the patient's records.
* **Two operations under one key.** Starting a conversation and sending its first message are two operations; give each its own key.
* **Clinical decisions.** A key makes a retry safe. It does not change who decides: every consultation is reviewed in full by an EU-licensed doctor, who decides on medical grounds only. See [Safety](https://docs.hi-doctor.ai/essentials/safety.md).

## How long are keys kept?

Keys are kept for 30 days per account and brand. After that the lookup returns `404` and a retry with the old key returns `409 idempotency_key_expired`, so an expired key is never silently run again.

## Frequently asked questions

### Do I have to send an Idempotency-Key?

No. The header is optional, and requests without it behave as before. Send it on any write you might retry, such as checkout, a message or a questionnaire submission.

### What should I use as the key?

Generate a random value, such as a UUID, when the user asks for the action, and store it with the pending operation. Keys are 8 to 128 letters, digits, underscores or hyphens.

### Can I reuse a key for a different request?

No. A key used for a different request returns 422 with the key reuse code. Use a new key only for a genuinely new operation.

### What does a 409 in-progress response mean?

The first attempt is still running. Wait, then retry with the same key; never create a new key to get around it.

### Does a successful key prove the payment or email went through?

No. It proves Hi Doctor stored the operation. Check the payment, message or consultation itself before telling the patient that something is done.

### How long is a key remembered?

Keys are kept for 30 days per account and brand. After that the lookup returns a not-found error and a retry with the old key is refused as expired.

## Next steps

* [Errors and recovery](https://docs.hi-doctor.ai/api-reference/errors.md): Recover from uncertain writes, throttles and timeouts.
* [Answer a questionnaire](https://docs.hi-doctor.ai/api-reference/questionnaire-guide.md): Guided answers use expected\_revision instead of a key.
* [Uploads and downloads](https://docs.hi-doctor.ai/api-reference/uploads-downloads.md): Upload photos and documents at most once.
* [MCP approvals](https://docs.hi-doctor.ai/mcp/approvals.md): The connector sends its operation ID as the key on these endpoints.


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