Skip to main content

Idempotency

The Flowlix API supports idempotency for safely retrying requests without performing the same operation twice. This is critical for payment processing, where network issues or timeouts can leave you unsure whether a request succeeded.

How it works

Include an Idempotency-Key header with a unique value on any POST request:
curl -X POST https://api.flowlix.dev/v1/payments \
  -H "Authorization: Bearer fl_test_sk_abc123def456" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -d '{"amount": 4999, "currency": "eur", "card": {...}}'

Behavior

  1. First request — Flowlix processes the request normally. If the response status is 2xx, the body is stored against the idempotency key. If the response is 4xx or 5xx, nothing is cached, so the same key may be retried.
  2. Retry with the same key and the exact same body — Flowlix returns the stored response. The status code, body, and content-type are replayed from cache; per-request headers (Flowlix-Request-Id, X-RateLimit-*) are regenerated for the retry, so they will not match the original response.
  3. Retry with the same key but a different body — Flowlix returns 409 Conflict with code idempotency_key_in_use. You cannot reuse a key with different parameters.
  4. Retry while another request with the same key is still in flight — Flowlix returns 409 Conflict with code idempotency_key_in_use and the message “A request with this idempotency key is currently being processed.” Wait briefly and retry.
Only successful (2xx) responses are cached. If a request fails with a client or server error, the idempotency slot is released, so you can immediately retry the same key with the same — or even a corrected — body.

Key requirements

RuleDetail
FormatAny string up to 255 characters. Random UUIDv4 works for one-shot requests; deterministic keys work for business-bound retries (see below).
ScopeKeys are scoped to the API key value used in the request. Rotating an API key resets the idempotency namespace.
ExpirationKeys expire after 24 hours. Cleanup runs hourly, so an entry can persist briefly past its TTL but is otherwise removed.
Applicable methodsHonored only on POST requests to /v1/*. On other methods the header is ignored — GET is naturally idempotent, so no key is needed.

Generating keys

For one-shot requests (e.g. a checkout button), a random UUIDv4 is fine:
import uuid
idempotency_key = str(uuid.uuid4())
# e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
const idempotencyKey = crypto.randomUUID();
// e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
String idempotencyKey = UUID.randomUUID().toString();
// e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
For requests bound to a specific business operation (e.g. refunding order #1234, creating a payment for cart #5678), prefer a deterministic key that derives from the operation identifier:
Idempotency-Key: refund-order-1234
Idempotency-Key: create-payment-cart-5678
This way, even if your service crashes and restarts mid-call, the retry will collapse against the original request instead of creating a new one.

Conflict errors

If you reuse an idempotency key with a different request body, or another request with the same key is still in flight, you get a 409 Conflict:
{
  "error": {
    "type": "idempotency_error",
    "code": "idempotency_key_in_use",
    "message": "Keys for idempotent requests can only be used with the same parameters they were first used with.",
    "param": null,
    "decline_code": null,
    "doc_url": "https://docs.flowlix.dev/api-reference/idempotency",
    "request_id": "req_abc123def456"
  }
}
For an in-flight conflict the message is instead:
A request with this idempotency key is currently being processed.
Both share the same code: "idempotency_key_in_use" and HTTP 409.

Best practices

  • Always use idempotency keys for payment creation and refund requests.
  • Generate a new key for each distinct operation — do not reuse keys across different payments or refunds.
  • Use deterministic keys for retries — if retrying a failed request, use the same key as the original attempt and resend the exact same body.
  • Do not regenerate the key on each retry — that defeats the purpose. The key must be the same across all attempts of the same operation.
  • Store the key alongside the operation in your database so you can retry consistently, even after restarts.
  • Treat 409 idempotency_key_in_use differently depending on the message: a body mismatch is a bug in your retry logic; an “in flight” conflict can simply be retried after a short delay.

Retry strategy

A recommended retry strategy for payment requests:
  1. Generate an idempotency key for the operation (random UUIDv4 or deterministic).
  2. Send the request.
  3. If you get a network timeout or 5xx error, wait and retry with the same idempotency key and the same body.
  4. Use exponential backoff: wait 1s, 2s, 4s, 8s, up to a maximum of 30s.
  5. After 3-5 retries, stop and alert your operations team.
import time
import requests
import uuid

def create_payment_with_retry(payload, api_key, max_retries=3):
    idempotency_key = str(uuid.uuid4())

    for attempt in range(max_retries + 1):
        try:
            response = requests.post(
                "https://api.flowlix.dev/v1/payments",
                headers={
                    "Authorization": f"Bearer {api_key}",
                    "Content-Type": "application/json",
                    "Idempotency-Key": idempotency_key,
                },
                json=payload,
                timeout=30,
            )

            if response.status_code < 500:
                return response.json()

        except requests.exceptions.RequestException:
            pass  # Network error -- will retry with the SAME key

        if attempt < max_retries:
            wait = min(2 ** attempt, 30)
            time.sleep(wait)

    raise Exception("Payment request failed after retries")