Skip to content

Retrying safely

When a write times out, you do not know whether it happened. Sending it again must not make a second booking for Anna Müller or book the same till sale twice. The API gives you three ways to make a repeat harmless: the Idempotency-Key header on any write, and a reference of your own on bookings and on till sales.

  1. Your system to Grondia: POST bookings/. With Idempotency-Key: 5f0c...

  2. Grondia to Your system: 201 Created. Lost on the way

  3. Your system: Timeout, so send the same request again

  4. Your system to Grondia: POST bookings/. With Idempotency-Key: 5f0c...

  5. Grondia to Your system: 201 Created. The stored answer

  6. Grondia: Still one booking

Add an Idempotency-Key header to a POST or PUT, with a new random value for each operation, such as a UUID. Keep the key and the exact body together until the operation has an answer.

Terminal window
curl https://api.campone.ch/api/v1/public/bookings/ \
-H "Authorization: Bearer ck_a1b2cXXXXXXXXXXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5f0c2d8e-7b1a-4c3e-9d6f-2a8b4c6e1f90" \
--data-binary @booking.json
  • The first answer in the 2xx range is stored, status and body, for this campground and this key.

  • The same key with the same request (method, path and body, byte for byte) gets that status and body back, and nothing runs a second time.

  • The same key with a different request answers 409:

    { "detail": "Idempotency-Key was reused with a different request.", "code": "idempotency_key_reuse" }
  • An error is not stored. A repeat after a 4xx or 5xx runs the request again.

What to watch for:

  • Byte for byte. Keep the bytes you sent and send those again. Building the JSON again from the same data can change the order of keys or the spacing, and that is a different request.
  • One request at a time. The answer is stored when the first request finishes. A repeat that arrives while the first one is still running is not recognised, and runs as well. Repeat only after the first attempt has failed or timed out, and for bookings also send an external_reference, which holds even then.
  • Keys belong to the campground, not to your API key, and they do not expire today. Use a fresh random value for every operation, never a counter or a value another integration could also pick. Grondia reads the first 200 characters.
  • A repeat counts against your rate limit like any request.

Send your own id for the booking as external_reference in POST bookings/ (up to 200 characters; spaces at either end are dropped). A campground has at most one booking per external_reference:

  • The first request makes the booking: 201.
  • A repeat with the same external_reference, from the same key, returns that booking with 200, guest data redacted as in any read (Personal data says why). Everything else in the repeat is ignored: it does not change the dates or the guests.
  • Two requests at the same moment cannot both make a booking. The second gets the first one’s booking with 200, or a 409 saying the pitch is taken; either way there is one booking.
  • If the booking with that reference was made by another key, or reached Grondia some other way, the answer is 409 with {"detail": "external_reference_conflict"}, and nothing about that booking.
  • The match is on the key that made the booking. After the campground replaces your key, a repeat of a booking made with the old key answers 409.

With an Idempotency-Key on the same request, the key is checked first: a repeat gets the stored 201.

Channel managers shows a push that survives a retry end to end.

POST bookings/{id}/cancel/ is not harmless to repeat by itself: the second call answers 400 with {"detail": "not_cancellable", "status": "cancelled"}. Send an Idempotency-Key to get the first answer, with its refund, back. Without one, treat not_cancellable with "status": "cancelled" as done.

POST pos/transactions/ requires your till’s id for the sale in external_ref (up to 128 characters, not blank). The first request records the sale: 201 and "idempotent_replay": false. A repeat with the same external_ref records nothing and returns the recorded sale: 200 and "idempotent_replay": true. The lines and amounts of the repeat are not compared with the first, so a new sale needs a new external_ref. To find out whether a sale arrived, ask GET pos/transactions/{external_ref}/: 404 with not_found means it did not. Shop and restaurant tills posts a sale this way, with the lines and the totals worked out.

  • PUT ari/restrictions/ is safe to repeat as it is: each row sets the rules of one site type on one date, and a row with every rule off removes them. Only the counts in the answer (upserted, deleted) can differ.
  • POST webhooks/ is keyed by the URL: a repeat updates the endpoint and answers 200 without the secret. If the answer to the first request was lost, the secret went with it; post the URL again with "rotate_secret": true to get a new one (see Webhooks).
  • POST webhooks/deliveries/{delivery_id}/replay/ queues one more delivery each time it is called. Send an Idempotency-Key if you repeat it.
What happened Retry?
No answer: a timeout or a dropped connection Yes: the same request, with the same Idempotency-Key
429 Yes, after the seconds in Retry-After (Rate limits)
503 or another 5xx Yes, after a pause, with the same Idempotency-Key
409 Not unchanged. Read the body (Errors): a taken pitch needs other dates or another pitch
400, 401, 403, 404, 422 No. Fix the request first. A date in the query that is not a real date is a 400 (bad dates in a query)

For the pause, our recommendation: start at one second, double it after each attempt, add a random part so that many clients do not retry in step, and stop after a few minutes and raise an alert.

The sandbox key is read-only, so none of these writes can be tried with it. For a key that may write, ask Felix, felix@grondia.com, or Siro, siro@grondia.com, and say what you are building.