Idempotent requests
How to make a mutation or an extension webhook delivery safe to retry with the Idempotency-Key header: what to send, what a replay returns, and what Awell answers when a key is reused or still in flight.
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: 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: 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
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.
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
422errors 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.
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
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
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
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
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:
{
"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."
}
}
}
]
}messageis the problem title, using the draft's wording.extensions.code,extensions.type, andextensions.statusCodefollow the same convention as every other error on this API. See Errors.extensions.problemcarries the problem details. Itstypelinks to this page.- The HTTP status matches
statusCode, and the response also carries aLink: <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
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, 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.
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/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
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.