ReferenceFor engineers

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 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.

What comes back

State of the keyThis requestResponse
UnusedAny mutationExecutes normally. The response is stored.
Held by a request still runningSame key409 Conflict, code IDEMPOTENCY_KEY_IN_FLIGHT. Nothing executes.
Used by any completed requestDifferent query, variables, or API key422 Unprocessable Content, code IDEMPOTENCY_KEY_REUSED. Nothing executes.
Used by a completed requestSame query, variables, and API keyThe stored response, with Idempotency-Replayed: true. Nothing executes.
Used, but the response was too large to storeSame query, variables, and API key200, code IDEMPOTENCY_REPLAY_UNAVAILABLE. Nothing executes.
AnyMalformed header value400 Bad Request, code IDEMPOTENCY_KEY_INVALID.
AnyThe key store is unreachable503 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."
        }
      }
    }
  ]
}
  • 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.
  • 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

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 keyThis deliveryResponse
UnusedAnyThe webhook runs. Its reply is stored.
Held by a delivery still runningSame key409 Conflict. Nothing runs.
Used by any completed deliveryDifferent body422 Unprocessable Content. Nothing runs.
Used by a completed deliverySame bodyThe stored reply, with Idempotency-Replayed: true. Nothing runs.
AnyMalformed header value400 Bad Request.
AnyThe key store is unreachable503 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.

On this page