Idempotency

Replay-safe mutations via Idempotency-Key, UUID v4, 24-hour window.

Idempotency

Every mutating request (POST, PUT, PATCH, DELETE) carries an
Idempotency-Key header. Within 24 hours, replays return the cached
response so your retries never double-charge, double-quote, or
double-send.

Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

The SDKs auto-mint a UUID v4 per call. You only need to think about
this if you call the API directly or need a stable key for cross-process
retry (e.g. resuming after a crash).

If you do override the key, mint a UUID v4 once per logical operation.
Reuse that same key across every retry of that operation; never share it
across different operations. Do not derive the key from a hash of the
request body (two semantically distinct requests can hash identically and
collide), and do not reuse a job ID or other application-domain string.

Scoping — keys are per-account

Idempotency keys are scoped per-account. The server's deduplication
composite is (account_id, Idempotency-Key, request_body_hash) — the
account is derived from your bearer token. Two different customers can
independently use the same key value: they live in disjoint namespaces,
and one customer's replay never returns another customer's cached
response.

This means you do not need to globally namespace your keys. A UUID v4
minted in your application is sufficient.

v3 enforces UUID v4 strictly

v3 enforcement — On /v3/* endpoints, the Idempotency-Key
header MUST be a UUID v4. Non-UUID strings (including
SHA-256-derived keys, job IDs, slugs, or any other application-domain
identifier) are rejected. Send a UUID v4 and you never see this.

If your key is rejected, you'll get a validation error naming
Idempotency-Key in the param field. Mint a fresh UUID v4 and retry:

POST /v3/prequalify
Idempotency-Key: job-2026-05-28-7421
Authorization: Bearer isa_test_4fjK2nQ7mX1aB8sR9pZ3
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "type":       "https://docs.isaapi.com/docs/errors-validation_error",
  "title":      "Validation Error",
  "status":     422,
  "code":       "validation_error",
  "param":      "Idempotency-Key",
  "detail":     "Idempotency-Key must be a UUID v4 on /v3 (got non-UUIDv4 value).",
  "doc_url":    "https://docs.isaapi.com/docs/errors-validation_error",
  "instance":   "/v3/prequalify",
  "livemode":   false,
  "request_id": "req_01HZK2N5GQR9T8X4B6FJW3Y1AS"
}

Today the server returns the body shown above. A future update will use
a distinct idempotency_key_format code and RFC 7807 response type.
For now, match on code === "validation_error" and
param === "Idempotency-Key". See Errors.

The SDKs mint UUID v4s automatically, so you only need to think about
this if you call the wire directly or override idempotencyKey. If you
do override, use your platform's UUID library (crypto.randomUUID(),
uuid.uuid4(), google/uuid, Guid.NewGuid(), Str::uuid()) — never
derive from a hash.

The 24-hour window

The server caches responses for 24 hours, keyed by
(account_id, Idempotency-Key, request_body_hash). Within this window:

  • Same key, identical body → cached response is returned. The server
    does not re-execute the operation.
  • Same key, different body409 Conflict with detail
    "idempotency key reused with different request body". Mint a new key
    for the new request. (The error contract reserves code: idempotency_conflict for this case,
    but the deployed server emits code: conflict. Match on HTTP 409 for now.)
  • After 24 hours → the cache entry expires. Reusing the key is safe
    but defeats the purpose; mint a new one.

The window is not configurable per request. It is the same 24 hours for
every endpoint and every account.

The response envelope echoes the key

Every successful response surfaces the key in the envelope alongside
request_id. This lets consumers capture the key without parsing headers
and lets the SDK's .withRawResponse() return an honest record.

{
  "object":          "prequalify_result",
  "request_id":      "req_01HZK2N5GQR9T8X4B6FJW3Y1AS",
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
  "livemode":        true,
  "data": { ... }
}

The header value and the envelope value are byte-identical. If they differ,
treat it as a protocol bug and file a ticket quoting both.

Retry policy

Reuse the same idempotency key across every retry of the same logical
operation. The SDKs handle this automatically; if you call the API
directly, follow these rules:

  • Network error before response → retry with the same key.
  • 429 / 5xx response → retry with the same key after backoff.
  • 4xx other than 429 → do not retry. The request is malformed
    and will fail the same way. Only mint a fresh key if you change the
    request.

Using a fresh key for each retry attempt is a bug. It bypasses
deduplication and can produce two real side effects (two charges, two
policy applications, two emails) for what the caller intended as one
operation.

Exemptions

Webhook receivers (/v1/webhooks/{provider}) do not use
Idempotency-Key. The provider's event.id is the deduplication anchor
and the platform de-dupes on event ID inside the receiver. Sending the
header is harmless and ignored.

Send the header on every other mutating operation — POST, PUT,
PATCH, DELETE. It makes your retries replay-safe. Without it, a
network retry can produce two real side effects.

The error contract reserves 400 missing_idempotency_key for a
mutating request without the header. The deployed /v3 server does not
yet enforce this. Always send the header anyway — it is the only thing
that makes a retry safe.

Code samples

Wire layer (curl). There is no auto-mint, so you set the
Idempotency-Key header directly. Mint a UUID v4 per logical operation
and reuse it across every retry. uuidgen produces a valid UUID v4 (case
is not significant):

# curl — set the key explicitly; the wire has no auto-mint.
curl -X POST https://zyins.isaapi.com/v3/prequalify \
  -H "Authorization: Bearer $ISA_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d @prequalify-request.json

SDK layer. The SDKs mint a fresh UUID v4 key on every call and echo
it back on the response envelope as idempotencyKey — capture it for
correlation without parsing headers:

import { Isa } from 'isa-sdk';
import type { PrequalifyRequest } from 'isa-sdk';

const isa = await Isa.withBearer();
declare const request: PrequalifyRequest;

const envelope = await isa.zyins.prequalify(request);
// The SDK minted and sent the key; the envelope echoes it verbatim.
console.log(envelope.idempotencyKey); // 550e8400-e29b-41d4-a716-446655440000
console.log(envelope.requestId);      // req_01HZK2N5GQR9T8X4B6FJW3Y1AS
from isa_sdk import Isa
from isa_sdk.zyins import PrequalifyRequest

isa = Isa.with_bearer()
request: PrequalifyRequest

result = isa.zyins.prequalify(request)
# The SDK minted and sent the key; the envelope echoes it verbatim.
print(result.idempotency_key)  # 550e8400-e29b-41d4-a716-446655440000
print(result.request_id)       # req_01HZK2N5GQR9T8X4B6FJW3Y1AS
// Go — the SDK mints the key and the envelope echoes it.
res, err := isa.Zyins.Prequalify.Run(ctx, req)
if err != nil {
    return err
}
fmt.Println(res.IdempotencyKey) // 550e8400-e29b-41d4-a716-446655440000
fmt.Println(res.RequestID)      // req_01HZK2N5GQR9T8X4B6FJW3Y1AS
// PHP — the SDK mints the key and the envelope echoes it.
$result = $isa->zyins->prequalify->run($input);
echo $result->idempotencyKey; // 550e8400-e29b-41d4-a716-446655440000
echo $result->requestId;      // req_01HZK2N5GQR9T8X4B6FJW3Y1AS
// C# / .NET — the SDK mints the key and the envelope echoes it.
var result = await isa.Zyins.PrequalifyAsync(req, ct);
Console.WriteLine(result.IdempotencyKey); // 550e8400-e29b-41d4-a716-446655440000
Console.WriteLine(result.RequestId);      // req_01HZK2N5GQR9T8X4B6FJW3Y1AS

See also

  • Errorsmissing_idempotency_key, idempotency_conflict.
  • Authentication — sending the bearer token.