TouricalDevelopers

Idempotency

Retry any write safely with the Idempotency-Key header.

Every POST / PATCH / DELETE under /v1/ honours the Idempotency-Key header, following draft-ietf-httpapi-idempotency-key-header.

How it works

  • Send Idempotency-Key: <opaque>. Recommended generator: UUID v4, one per logical operation.
  • The first final response (any 2xx or 4xx) is stored against the key, scoped to your tenant, for 24 hours.
  • A replay within that window — the duplicate POST your client sends after a network timeout — returns the stored response byte-for-byte with Idempotency-Replayed: true. No second side-effect.
  • The same key with a different request body (or method/path) returns 409 with problem type idempotency-key-reuse.
  • A replay while the first request is still running waits briefly for it to finish and replays its response; if it has not finished you get 409 with problem type idempotency-in-progress — back off and retry with the same key.
  • 5xx responses are not stored. The key is released so your retry re-executes the request instead of replaying the outage.
curl -X POST https://app.tourical.com/api/v1/clients \
  -H "Authorization: Bearer tk_live_..." \
  -H "Idempotency-Key: 4c1b1a8e-3f0c-4d0f-9d8b-6d2b7e0a1f11" \
  -H "Content-Type: application/json" \
  -d '{ "email": "ana@example.com", "firstName": "Ana" }'

The wire format matches Stripe's contract closely: if you already retry-with-idempotency-key against Stripe, the work to support Tourical is changing the URL and the header name.

Retry pattern

async function createClientOnce(input: ClientInput, key = crypto.randomUUID()) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch("https://app.tourical.com/api/v1/clients", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
        "Idempotency-Key": key,          // same key on every attempt
      },
      body: JSON.stringify(input),
    });
    if (res.status < 500 && res.status !== 429) return res; // final (stored) answer
    await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
  }
  throw new Error("gave up");
}

What is retry-safe without a key?

  • All GET endpoints.
  • DELETE /v1/webhooks/{id} — a second delete returns 404.
  • POST /v1/webhooks/test — retrying sends another test delivery, by design.
  • POST /v1/webhooks/{id}/deliveries/{deliveryId}/redeliver — each call queues one more delivery of the same event; your consumer dedupes on Tourical-Event-Id.

On this page