# Idempotent requests
URL: https://docs.awellhealth.com/api-reference/guides/idempotency

> For the complete documentation index, see [llms.txt](https://docs.awellhealth.com/llms.txt).



A client that retries a mutation after a timeout cannot tell whether the first attempt ran. Sending
an `Idempotency-Key` header on the mutation lets the Orchestration API tell it: a retry of a request
that already completed returns the original response, and the mutation does not run a second time.

The same header works on [extension webhooks](#extension-webhooks): a system that delivers an event
to an Awell extension webhook URL can retry the delivery without the event being processed twice.

The header is optional. Without it, every request executes and nothing on this page applies.

The behavior follows the IETF HTTPAPI draft
[The Idempotency-Key HTTP Header Field](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header):
the header name, the quoted value, the status codes, and the wording of the errors. Where GraphQL
forces a different shape, this page says so.

## Send the header [#send-the-header]

Add `Idempotency-Key` to a mutation request. The value is an RFC 8941 String: double-quoted, with
`\"` and `\\` as the only escapes. Inside the quotes, use 1 to 255 printable ASCII characters with
no whitespace. A UUID generated per logical request is the recommended value.

```bash
curl https://api.sandbox.awellhealth.com/orchestration/m2m/graphql \
  -H 'Content-Type: application/json' \
  -H 'apiKey: YOUR_API_KEY' \
  -H 'Idempotency-Key: "6f1c2e0e-3f0b-4c1e-9a1b-0d2c9c0f7b21"' \
  -d '{
    "query": "mutation StartPathway($input: StartPathwayInput!) { startPathway(input: $input) { code success pathway_id } }",
    "variables": { "input": { "pathway_definition_id": "DEFINITION_ID", "patient_id": "PATIENT_ID" } }
  }'
```

If this request times out and is sent again with the same key, the second attempt returns the
`pathway_id` the first one created. Without the key, the retry starts a second care flow for the
patient.

Rules for the value:

* One key per logical request. A retry reuses the key; a new request gets a new key.
* Retry with the same query, the same variables, and the same API key. The API compares all three.
* Use a key that cannot collide with one from another system or job sending requests to the same
  organization, such as a UUID. Keys derived from a shared identifier, such as a record ID, can meet
  each other and produce `422` errors that look like bugs.
* Send the quoted form only. A bare value, a value with parameters, or anything else the grammar
  rejects is refused with `400`.

Queries ignore the header. Only mutations are guarded.

### From the SDK [#from-the-sdk]

The [server-side SDK](/api-reference/tools/api-tools#server-side-sdk) sends the header for you:
`sdk.withIdempotencyKey(key)` returns an orchestration client whose every request carries that key,
quoted as above.

Where this pays off is a handler that makes more than one call. Say an incoming event makes you
create a patient and then complete an activity, and the second call fails. Whatever retries the
handler, your queue or the system that sent the event, runs the first call again too, and creates a
second patient. With a key on each call, the retried first call replays the patient it already
created and only the failed call actually executes. Derive one key per call from the identity of the
event, such as `${event.id}:patient` and `${event.id}:complete`, so a retry of the same event reuses
them and a different event never can.

```javascript
const client = sdk.withIdempotencyKey(`${event.id}:patient`)
const result = await client.mutation({
  createPatient: {
    __args: { input: { first_name, last_name, email } },
    patient: { id: true },
  },
})
```

Refusals arrive as an `AwellApiError` carrying the HTTP `status` and the `code` from the table
below, so a caller can branch on them instead of parsing a message:

```javascript
import { AwellApiError } from '@awell-health/awell-sdk'

try {
  await client.mutation({ ... })
} catch (err) {
  if (err instanceof AwellApiError && err.code === 'IDEMPOTENCY_KEY_IN_FLIGHT') {
    // the first attempt is still running: retry shortly
  }
  throw err
}
```

`AwellApiError` is thrown for any response that carries GraphQL errors, on any HTTP status, and
extends the generated client's `GenqlError`, so existing error handling keeps working. To tell a
replay from a fresh execution, pass `{ onResponse }` as the second argument; it receives the status
and whether the response was replayed.

## What comes back [#what-comes-back]

| State of the key                              | This request                           | Response                                                                           |
| --------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------- |
| Unused                                        | Any mutation                           | Executes normally. The response is stored.                                         |
| Held by a request still running               | Same key                               | `409 Conflict`, code `IDEMPOTENCY_KEY_IN_FLIGHT`. Nothing executes.                |
| Used by any completed request                 | Different query, variables, or API key | `422 Unprocessable Content`, code `IDEMPOTENCY_KEY_REUSED`. Nothing executes.      |
| Used by a completed request                   | Same query, variables, and API key     | The stored response, with `Idempotency-Replayed: true`. Nothing executes.          |
| Used, but the response was too large to store | Same query, variables, and API key     | `200`, code `IDEMPOTENCY_REPLAY_UNAVAILABLE`. Nothing executes.                    |
| Any                                           | Malformed header value                 | `400 Bad Request`, code `IDEMPOTENCY_KEY_INVALID`.                                 |
| Any                                           | The key store is unreachable           | `503 Service Unavailable`, code `IDEMPOTENCY_STORE_UNAVAILABLE`. Nothing executes. |

**A key is never a reason to execute twice.** Every row above either runs the mutation exactly once
or refuses to run it.

### Replays [#replays]

A replay returns the response the original request received, as a normal GraphQL response body,
plus an `Idempotency-Replayed: true` header. The header is the only way to tell a replay from a
fresh execution. The draft defines no replay indicator; this header is an Awell addition.

### Failures are outcomes too [#failures-are-outcomes-too]

A mutation that fails with a client error is replayed like a success, because the retry would fail
the same way. Client errors are those whose GraphQL errors all carry a 4xx `statusCode` in
`extensions`, such as a validation failure, a missing record, or a denied permission, plus argument
validation failures. To send a corrected request, use a new key.

A mutation that fails for any other reason, such as a `5xx` or an error with no status, releases the
key. The retry then runs for real, so a transient failure stays retryable.

### Concurrent duplicates [#concurrent-duplicates]

While the first request with a key is still running, a second request with the same key gets `409`
and is not queued. Retry after a short pause to receive the replay. Two requests with **different**
keys that touch the same record are two separate requests; both execute, and the mutation's own
rules decide the result.

## How errors are reported [#how-errors-are-reported]

The draft describes these errors as RFC 9457 problem details. A GraphQL endpoint answers with a
GraphQL response, so the same content arrives inside the error object:

```json
{
  "errors": [
    {
      "message": "Idempotency-Key is already used",
      "extensions": {
        "code": "IDEMPOTENCY_KEY_REUSED",
        "type": "IDEMPOTENCY_KEY_REUSED",
        "statusCode": 422,
        "problem": {
          "type": "https://docs.awellhealth.com/api-reference/guides/idempotency",
          "title": "Idempotency-Key is already used",
          "detail": "Idempotency Key MUST not be reused across different payloads of this operation."
        }
      }
    }
  ]
}
```

* `message` is the problem title, using the draft's wording.
* `extensions.code`, `extensions.type`, and `extensions.statusCode` follow the same convention as
  every other error on this API. See [Errors](/api-reference/guides/errors).
* `extensions.problem` carries the problem details. Its `type` links to this page.
* The HTTP status matches `statusCode`, and the response also carries a
  `Link: <https://docs.awellhealth.com/api-reference/guides/idempotency>; rel="describedby"` header.

Unlike most errors on this API, these arrive with a `4xx` or `5xx` status. A client that branches on
the HTTP status can distinguish an idempotency refusal from a normal GraphQL failure.

## Extension webhooks [#extension-webhooks]

The header also guards the URLs Awell issues for **extension webhooks**: the incoming webhooks of
type *Extension webhook* described in
[Start a care flow from an incoming webhook](/docs/automate-with-events/start-a-care-flow-from-an-incoming-webhook),
at the URL Studio shows when you enable one. Both the current and the older URL format are covered. A
webhook URL you post to directly, the other webhook type on that screen, does not read the header
yet.

Send `Idempotency-Key` with the delivery, in the same quoted form as above. One key per event;
a redelivery of the same event reuses it.

```bash
curl https://EXTENSION_WEBHOOK_URL \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: "8e03978e-40d5-43e8-bc93-6894a57f9324"' \
  -d '{ "event": "appointment.booked", "id": "APPOINTMENT_ID" }'
```

Keys are unique per webhook URL. Two systems delivering to the same URL share a key space, so use a
value that cannot collide between them, such as a UUID, rather than a shared record ID. "The same
delivery" means the same JSON body to the same URL; whitespace and key order in the body do not
matter.

| State of the key                 | This delivery                | Response                                                           |
| -------------------------------- | ---------------------------- | ------------------------------------------------------------------ |
| Unused                           | Any                          | The webhook runs. Its reply is stored.                             |
| Held by a delivery still running | Same key                     | `409 Conflict`. Nothing runs.                                      |
| Used by any completed delivery   | Different body               | `422 Unprocessable Content`. Nothing runs.                         |
| Used by a completed delivery     | Same body                    | The stored reply, with `Idempotency-Replayed: true`. Nothing runs. |
| Any                              | Malformed header value       | `400 Bad Request`.                                                 |
| Any                              | The key store is unreachable | `503 Service Unavailable`. Nothing runs.                           |

"Nothing runs" means exactly that: the extension's webhook code is not invoked, no care flow is
started or advanced, and no webhook log entry is written for the retry. A replay returns the HTTP
status and body the webhook originally answered with. A reply the webhook chose with a `4xx` status
is stored and replayed too, as with mutations; a `5xx` releases the key so the retry runs for real.

Because these are plain HTTP endpoints, refusals arrive as RFC 9457 problem details rather than a
GraphQL error:

```http
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
Link: <https://docs.awellhealth.com/api-reference/guides/idempotency>; rel="describedby"

{
  "type": "https://docs.awellhealth.com/api-reference/guides/idempotency",
  "title": "Idempotency-Key is already used",
  "status": 422,
  "detail": "Idempotency Key MUST not be reused across different payloads of this operation.",
  "code": "IDEMPOTENCY_KEY_REUSED"
}
```

`code` takes the same values as `extensions.code` does for mutations. The expiry rules below apply
to webhook deliveries as well.

## Expiry [#expiry]

A completed key is remembered for **7 days**. A retry after that executes again, as a new request.
While a request is running, its key is held for at most **60 seconds**; if the request is still
running past that, a retry is no longer refused with `409` and may run alongside it.

The stored response is kept up to 5 MiB. A larger response is not kept, and a replay of it is
refused with `IDEMPOTENCY_REPLAY_UNAVAILABLE` rather than re-executed. Read the current state with a
query instead of retrying.

## Outgoing API calls and webhooks [#outgoing-api-calls-and-webhooks]

Awell also sends the header, on both kinds of request a care flow makes to your systems:

* An [API call action](/docs/connect-systems/the-api-call-action-graphql-and-rest#every-call-carries-an-idempotency-key)
  sends `Idempotency-Key` with the id of that API call activity as the value.
* A [webhook](/api-reference/guides/webhooks) delivery sends `Idempotency-Key` with the id of that
  webhook call as the value.

Both use the same quoted form, and there is nothing to configure.

```http
POST /your/endpoint
Content-Type: application/json
Idempotency-Key: "5f3d1c2e9b7a4d0f8e6c1a2b"
```

The value is the same on every retry of that call, whether Awell retries automatically or someone
retries it from Awell Care, so your endpoint can treat a repeated key as the same request and answer
with the result it already produced instead of acting twice. A different activity or delivery always
carries a different key, and a test call made while designing an API call action gets a new id each
time.

## Related [#related]

* [The API call action](/docs/connect-systems/the-api-call-action-graphql-and-rest)
* [Receive webhook events](/docs/automate-with-events/receive-webhook-events)
* [Start a care flow from an incoming webhook](/docs/automate-with-events/start-a-care-flow-from-an-incoming-webhook)
* [Errors](/api-reference/guides/errors)
* [Mutations](/api-reference/guides/mutations)
* [Rate limits](/api-reference/guides/rate-limits)
