Idempotency

This article and the rest of the API documentation in this section are written for a technical audience — integrators and developers connecting external systems to Tickiti. Familiarity with HTTP, REST, JSON and bearer-token authentication is assumed.

Network calls fail. They time out, drop connections, return 502 from a flaky proxy. When that happens to a write request, the calling system has to decide whether to retry — and risk creating duplicates — or back off and risk losing the work.

Tickiti solves this with idempotency keys. Every API write requires an Idempotency-Key header. The key is a unique identifier for the logical operation; Tickiti remembers it for a long-enough window that a retry with the same key replays the original outcome rather than redoing the work.

The contract

  1. Same key + same payload → Tickiti returns the previously stored response. Safe to retry as many times as you like.
  2. Same key + different payload → Tickiti returns 409 idempotency_key_conflict. Always a bug on the caller side — the key has been reused with the wrong content.
  3. Different key → Tickiti executes the request normally.

The header is mandatory on the legacy write endpoints: POST /api/create_ticket, POST /api/ticket_respond, POST /api/send_email and POST /api/render_template. The unified endpoints (POST /api/tickets/create and POST /api/tickets/{number}/respond) do not require Idempotency-Key — they trade off the safety net for a leaner request shape, so if you need retry safety you should be using the legacy endpoints.

Generating keys

Two options:

  1. Generate locally — any UUIDv4 (or other unique-enough string up to 255 chars) works. The simplest pattern: pin the key to the logical operation in your system. order:SP-2412-0489:warranty as a deterministic key means a retry of the same warranty filing reuses the same key automatically.
  2. Pre-create via TickitiPOST /api/idempotency_key returns a fresh UUID-based key bound to a specific action (send_email or create_ticket). Use this if you want Tickiti to mint the key for you.

Pre-creation example:

curl -X POST https://support.sole-provider.example/api/idempotency_key \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "action": "create_ticket" }'

# response:
{ "ok": true, "data": { "idempotency_key": "7b4f3c2e-9d6a-4f8b-9e7c-2c3a4b5e6f7a", "action": "create_ticket" } }

Retry strategy

  1. Generate (or fetch) the key once at the start of the logical operation.
  2. Send the request with that key.
  3. On a 5xx, network timeout, or no response, retry with the same key. Repeat with exponential backoff up to your retry budget.
  4. On a 2xx, you are done.
  5. On a 4xx other than 409, do not retry — the request will fail again.
  6. On a 409 idempotency_key_conflict, the key was reused with a different payload. This is a bug; do not retry; investigate which earlier request collided.

Retention window

Idempotency records are pruned by housekeeping on a schedule:

  1. Completed records — kept for idempotency.retention_days (default 7 days) past completion, then removed. A retry of a key older than that is treated as a brand-new request, so plan retries to fit comfortably inside the window.
  2. In-flight records — if a key was created but never completed (the server crashed mid-request, say), the stale record is cleared after idempotency.stale_inflight_hours (default 6 hours) so it cannot block a fresh attempt forever.

Both windows are configured in config/idempotency.php on the Tickiti install; the defaults are conservative and rarely need changing.

Common mistakes

  1. Generating a fresh UUID inside the retry loop. The whole point is the key stays the same across retries. Generate once.
  2. Reusing a key across logically different operations. Same key + different payload = 409. Each logical operation needs its own key.
  3. Treating 409 as "try again with a new key". It is not — it means you have a bug. Surface it; do not paper over it.