1. Guides
  2. Idempotency and conditional writes

Networks fail partway through requests. This guide covers three ways to stay safe when that happens:

  • Idempotency keys make retrying a create safe.
  • Conditional writes stop you overwriting a change someone else made.
  • Receiver-side idempotency makes it safe for your receiver to get the same callback twice.

​
Idempotency keys

Send an Idempotency-Key header, and retrying the request cannot create a second resource.

Two operations read the header:

OperationHeader
schedules.createoptional
occurrences.redeliverrequired

No other operation reads it. Sending one elsewhere changes nothing.

​
Choosing a key

  • 16 to 255 characters, all printable ASCII (space through tilde).
  • Generate a new key for each schedule you mean to create. Reuse a key only to retry that same request.
  • Keys are scoped to your organization, project, environment, and the operation, so another project can use the same key without conflict.
  • Greatwait keeps a key’s receipt for 24 hours. Do not count on a retry being recognised after that.

​
What a retry returns

You sendGreatwait answers
A new keyCreates the resource.
The same key with the same requestCreates nothing. Returns the resource that key created, with the response header Idempotency-Replayed: true.
The same key with a different requestCreates nothing. Refuses with 409 and code idempotency_key_reused.

Two requests count as the same if they differ only in JSON key order or insignificant whitespace.

A replay returns the resource as it is now, not a copy of the first response. If the schedule was paused in the meantime, the replay shows it paused. Idempotency-Replayed is either true or absent. It is never false.

​
Example

This creates a one-shot, then repeats the identical request. It expects GREATWAIT_API_KEY and GREATWAIT_ENDPOINTID to be set in your environment already. curl reads the API key from the environment itself (curl 8.3.0 or later), so the key never appears on the command line.

GREATWAIT_API_URL="https://api.greatwait.dev"
GREATWAIT_IDEMPOTENCY_KEY="first-value-$(date +%s)-$"
BODY='{"delaySeconds":30,"endpoint":"'"$GREATWAIT_ENDPOINTID"'"}'

for try in 1 2; do
  curl -sS -D - -o /dev/null -X POST "$GREATWAIT_API_URL/v1/schedules" \
    --variable %GREATWAIT_API_KEY \
    --expand-header "Authorization: Bearer {{GREATWAIT_API_KEY}}" \
    -H "Idempotency-Key: $GREATWAIT_IDEMPOTENCY_KEY" \
    -H 'Content-Type: application/json' \
    --data-binary "$BODY"
done

The first response is 201. The second carries Idempotency-Replayed: true and describes the same schedule.

In JavaScript, the requestDetailed method of the @greatwait/api-client client returns response headers alongside the body, which is how you read Idempotency-Replayed.

​
When to retry

Retry schedules.create with the same key when the request failed without an answer, or was refused with rate_limited or service_unavailable. For any other refusal, fix the request first. A changed body needs a new key.

​
Conditional writes

Schedules and endpoints carry a version. Reading one returns it as a strong ETag header, such as ETag: "7". Every successful write returns the new ETag.

To write only if nothing has changed since you read, send that value back in If-Match:

OperationIf-Match
schedules.replaceoptional
schedules.pauseoptional
schedules.resumeoptional
schedules.canceloptional
schedules.archiveoptional
endpoints.updateoptional
endpoints.rotateSigningKeyoptional
endpoints.acknowledgeSigningKeyRotationrequired
  • If the version still matches, the write goes ahead.
  • If someone else changed the resource first, the write is refused with 409 and code version_conflict. Nothing is written. Read the resource again, decide whether your change still makes sense, and retry with the new ETag.
  • Only strong tags are accepted. A weak tag such as W/"7", or *, is refused with invalid_request.
  • A write without If-Match is unconditional: the last writer wins.

A different 409, state_conflict, means the resource is in a state where the operation cannot apply at all, for example resuming a cancelled schedule. Re-reading will not help.

# Read the schedule and keep its ETag.
ETAG=$(curl -sS -D - -o /dev/null "$GREATWAIT_API_URL/v1/schedules/$GREATWAIT_SCHEDULEID" \
  --variable %GREATWAIT_API_KEY \
  --expand-header "Authorization: Bearer {{GREATWAIT_API_KEY}}" \
  | sed -n 's/^[Ee][Tt][Aa][Gg]: *//p' | tr -d '\r')

# Pause it only if nobody changed it since.
curl -sS -X POST "$GREATWAIT_API_URL/v1/schedules/$GREATWAIT_SCHEDULEID/pause" \
  --variable %GREATWAIT_API_KEY \
  --expand-header "Authorization: Bearer {{GREATWAIT_API_KEY}}" \
  -H "If-Match: $ETAG"

​
Receiver-side idempotency

Callback delivery is at least once. Your receiver can get the same occurrence more than once: after a timeout, a dropped connection, or a retry following an answer Greatwait never received. Each of those deliveries has a new delivery id and the same occurrence id.

So your receiver should:

  • ignore a delivery id it has already handled,
  • run the work for an occurrence at most once for automatic deliveries,
  • run it again when a person orders a redelivery.

Verifying callbacks shows how, and @greatwait/api-client does all three for you.