Skip to main content

Idempotency

Network failures happen. A write request that times out, gets a 5xx, or loses its connection mid-flight leaves the client unsure whether the operation completed. Naively retrying could double-charge a refund, send duplicate cancellation emails, or fire a registration transfer twice.

The Viewcy API supports the Idempotency-Key HTTP header on every write endpoint (POST, PATCH, DELETE). Retrying a request with the same key returns the original response instead of executing the operation again.

Using the header is optional. Requests without it behave exactly as a normal API call. We strongly recommend setting one on every write.

How it works

  1. Generate a unique key for each logical operation — a UUIDv4 is the conventional choice.
  2. Send the key in the Idempotency-Key request header.
  3. The first request runs normally. We cache the response (status code, body, content type) for 24 hours, scoped to your API token's owner.
  4. Any subsequent request with the same key replays the cached response without re-executing the operation.

Every response to a request that carried a valid Idempotency-Key includes two signals:

  • Response header Idempotency-Replayed: true is set when the response was served from the idempotency cache (handy for proxies and logs that don't parse JSON).
  • Meta envelope meta.idempotencyKey echoes the client-supplied key, and meta.idempotentReplayed is true for replays and false for fresh execution.

The replay carries a fresh meta.requestId (it is a distinct HTTP request), so two replays of the same operation will not share a request ID. Use meta.idempotencyKey to correlate retries.

Request

POST /2025_06/events HTTP/1.1
Host: api.viewcy.com
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
Idempotency-Key: 4f8a2b3c-9d1e-4a5b-8c7d-1e2f3a4b5c6d

{ "name": "My Event", "category": "Concerts" }

Key requirements

  • Format: any string between 1 and 255 characters. UUIDs are conventional; opaque strings work too.
  • Uniqueness: scoped to the resource owner (the User or School your API token authenticates as). Two different accounts can use the same key independently; within one account, reusing a key replays the original response.
  • Lifetime: 24 hours from the first request. After that, the same key can be used again for a new operation.

Response codes you may see

StatusWhenMeaning
<original status>Replay of a completed requestThe first request's status and body are returned. Idempotency-Replayed: true header is set; meta.idempotentReplayed is true; meta.requestId is fresh.
400 Bad RequestHeader is empty or longer than 255 charactersFix the header value and retry with a valid key.
409 ConflictAnother request with the same key is still being processedThe first call hasn't finished yet. Retry after a short backoff.
422 Unprocessable EntitySame key, different requestA previous request used this key with a different method, path, query string, or body. Either resend the original request to get the cached response, or generate a new key for this new operation.
503 Service UnavailableIdempotency cache is temporarily unavailableRetry after a brief delay. The write was not executed.

Example 2xx replay body (note meta.idempotentReplayed: true):

{
"object": "event",
"id": "9b2c1d4e-7f3a-4c5b-8d6e-1a2b3c4d5e6f",
"meta": {
"requestId": "req_def456",
"idempotencyKey": "4f8a2b3c-9d1e-4a5b-8c7d-1e2f3a4b5c6d",
"idempotentReplayed": true
}
}

Example 409 body:

{
"object": "error",
"status": 409,
"message": "A request with this Idempotency-Key is currently being processed. Retry after a brief delay.",
"errors": [{ "code": "idempotency_in_progress" }],
"meta": {
"requestId": "req_...",
"idempotencyKey": "4f8a2b3c-9d1e-4a5b-8c7d-1e2f3a4b5c6d",
"idempotentReplayed": false
}
}

Example 422 body:

{
"object": "error",
"status": 422,
"message": "This Idempotency-Key was previously used with a different request. Use a new key for a different operation, or resend the original request.",
"errors": [{ "code": "idempotency_key_mismatch" }],
"meta": {
"requestId": "req_...",
"idempotencyKey": "4f8a2b3c-9d1e-4a5b-8c7d-1e2f3a4b5c6d",
"idempotentReplayed": false
}
}

meta.idempotencyKey and meta.idempotentReplayed appear together on every response to a write request that carried a valid header — including 409, 422, and 503. They are absent on 400 idempotency_key_invalid (the header didn't pass validation, so there's nothing well-formed to echo).

What we cache, what we don't

  • Cached and replayed: successful responses (2xx) and deterministic client errors (400, 404, 409, 422). A retry can't change a payload-rejected validation result, so we replay it.
  • Not cached: 5xx server errors (so transient failures don't block recovery); 401 and 403 (auth state can legitimately change between retries — a token rotation or scope grant should be reflected, not replayed); 429 (rate-limit windows shift); and the 409 idempotency_in_progress response itself (so the key isn't poisoned by a transient collision).
  • Response body size limit: responses larger than 1 MB are not cached. The original response still returns to the client; a retry with the same key will re-execute the operation. Bulk exports and very large list endpoints are the typical case.
  • Cache unavailable: if the idempotency cache cannot be read or a lock cannot be claimed before the write runs, the API returns 503 idempotency_unavailable and does not execute the write.

What the header does not do

  • It is not authentication. The key doesn't prove identity; your bearer token still does.
  • It is not a confirmation token. It does not authorize destructive operations or bind a payload signature.
  • It is not a transaction ID. Use your own correlation ID for cross-system tracking.
  • It does not apply to GET requests. Reads are already idempotent — sending the header on a GET is a no-op.
  • It does not apply to POST /uploads. Direct-upload responses carry short-lived presigned URLs; replaying a cached one could hand back an expired URL. Re-request a fresh upload instead — it's cheap. See File uploads.

Generate the key at the call site, before the first attempt, and reuse it across every retry of the same logical operation:

# Ruby
key = SecureRandom.uuid
3.times do |attempt|
response = api.post("/events", body: payload, headers: { "Idempotency-Key" => key })
break if response.success?
sleep(2 ** attempt) # exponential backoff
end

A new key per logical operation, the same key across every retry of that operation.

See also