Overview

All state-changing endpoints in the OpenFX v1 API (POST, PATCH, DELETE) require an Idempotency-Key header. This ensures that retrying a failed request — whether due to a network timeout, a client crash, or an ambiguous response — will never create duplicate resources or apply an operation twice.
Idempotency is especially critical for financial operations. Without it, a network timeout during a payment creation could leave you unsure whether the payment was created, and retrying could create a duplicate payment. The Idempotency-Key header eliminates this class of failure.

How It Works

  1. Generate a unique key for each distinct operation (UUID v4 recommended).
  2. Include the key in the Idempotency-Key request header.
  3. If the request succeeds, the server stores the response keyed by your idempotency key.
  4. If you retry with the same key and body, the server returns the stored response without re-executing the operation.
curl -X POST https://sandbox.api.openfx.com/v1/payments \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{
    "sourceAccountId": "acc_01953e1a5f4b7002",
    "counterpartyId": "cpt_01953e1a5f4b7004",
    "paymentMethodId": "pm_01953e1a5f4b7300",
    "rail": "ach",
    "sendAmount": { "currency": "USD", "value": "150000" }
  }'

Key Requirements

RequirementDetail
Header nameIdempotency-Key
Required onAll POST, PATCH, and DELETE requests
FormatAny string up to 128 characters. UUID v4 recommended.
RetentionKeys are stored for 24 hours after the original request.
ScopeKeys are scoped to your API credentials. Different API keys have independent key spaces.

Behavior Scenarios

ScenarioWhat HappensResponse
New requestRequest is processed normally.Normal response (e.g., 201 Created).
Replay (same key + same body)Original response is returned from cache.Original response + Idempotency-Replayed: true header.
Conflict (same key + different body)Request is rejected.409 with error code duplicate_idempotency_key.
In-flight (same key, original still processing)Request is rejected with a retry hint.409 with error code idempotency_key_in_flight + Retry-After header.
Expired key (after 24 hours)Treated as a new request.Normal response.

Replay Response

When a response is served from the idempotency cache, the Idempotency-Replayed header is set to true:
HTTP/1.1 201 Created
Idempotency-Replayed: true
Content-Type: application/json

{
  "id": "pmt_01953e1a5f4b7005",
  ...
}
The response body, status code, and headers match the original response exactly.

Conflict Response

If you reuse an idempotency key with a different request body, the API returns a 409:
{
  "error": {
    "type": "idempotency_error",
    "code": "duplicate_idempotency_key",
    "message": "An idempotency key was reused with a different request body. Generate a new key for distinct operations.",
    "status": 409,
    "requestId": "req_01953e1a5f4b7b01",
    "retryable": false
  }
}

In-Flight Response

If the original request is still being processed when you retry, the API returns a 409 with a Retry-After header:
HTTP/1.1 409 Conflict
Retry-After: 5
Content-Type: application/json

{
  "error": {
    "type": "idempotency_error",
    "code": "idempotency_key_in_flight",
    "message": "A request with this idempotency key is currently being processed. Retry after the indicated delay.",
    "status": 409,
    "requestId": "req_01953e1a5f4b7b02",
    "retryable": true
  }
}
When you receive an idempotency_key_in_flight error, wait for the number of seconds indicated in the Retry-After header before retrying. The original request is still in progress and will complete shortly.

Code Example: Idempotent Payment Creation with Retry

import uuid
import time
import requests

BASE_URL = "https://sandbox.api.openfx.com/v1"

def create_payment_idempotent(headers: dict, payment_data: dict, max_retries: int = 3):
    """Create a payment with idempotency and automatic retry logic."""
    idempotency_key = str(uuid.uuid4())
    headers = {**headers, "Idempotency-Key": idempotency_key}

    for attempt in range(max_retries + 1):
        try:
            response = requests.post(
                f"{BASE_URL}/payments",
                headers=headers,
                json=payment_data,
                timeout=30,
            )

            if response.status_code in (201, 202):
                replayed = response.headers.get("Idempotency-Replayed") == "true"
                if replayed:
                    print(f"Response served from idempotency cache")
                return response.json()

            if response.status_code == 409:
                error = response.json()["error"]

                if error["code"] == "idempotency_key_in_flight":
                    retry_after = int(response.headers.get("Retry-After", 5))
                    print(f"Request in flight, retrying in {retry_after}s...")
                    time.sleep(retry_after)
                    continue

                if error["code"] == "duplicate_idempotency_key":
                    raise ValueError(
                        "Idempotency key conflict: same key used with different body"
                    )

            response.raise_for_status()

        except requests.exceptions.Timeout:
            if attempt < max_retries:
                # Safe to retry with the same idempotency key
                wait = 2 ** attempt
                print(f"Timeout, retrying in {wait}s (attempt {attempt + 1})...")
                time.sleep(wait)
                continue
            raise

    raise RuntimeError(f"Failed after {max_retries + 1} attempts")


# Usage
payment = create_payment_idempotent(
    headers=auth_headers,
    payment_data={
        "sourceAccountId": "acc_01953e1a5f4b7002",
        "counterpartyId": "cpt_01953e1a5f4b7004",
        "paymentMethodId": "pm_01953e1a5f4b7300",
        "rail": "ach",
        "sendAmount": {"currency": "USD", "value": "150000"},
    },
)
print(f"Payment created: {payment['id']}")

Best Practices

Generate a new UUID v4 for every distinct operation. Each unique business action (create a payment, update a counterparty, etc.) should have its own idempotency key.
Reuse the same key when retrying the same operation. If a request fails due to a network error or timeout, retry with the exact same idempotency key and request body. This is what makes retries safe.
Store the idempotency key alongside the request in your system. If your process crashes between sending the request and recording the result, you can recover by replaying the same key.
Never reuse an idempotency key across different operations. Using the same key for a payment creation and then a transfer creation will result in a 409 duplicate_idempotency_key error on the second request.
Do not reuse keys after the 24-hour retention window. After 24 hours, the server discards the cached response. A retry after this window will be treated as a new request, potentially creating a duplicate. Design your retry logic to complete well within the 24-hour window.