Skip to main content

What is Idempotency?

Idempotency means you can safely retry the same request multiple times without creating duplicates. This is critical for:
  • Network failures and timeouts
  • Uncertain transaction states
  • Retry logic in distributed systems
  • Preventing accidental duplicates

Two Idempotency Mechanisms

1. External ID Pattern

Use external_id field to ensure resources are created only once:
{
  "external_id": "your-system-id-123",
  "legal_name": "Example Ltd",
  ...
}
Behavior:
  • First request: Creates resource, returns 201 Created
  • Retry with same external_id: Returns existing resource, returns 200 OK
The external_id should be unique within your system and stable (don’t change it).

2. Idempotency-Key Header

For operations that don’t support external_id, use the Idempotency-Key header:
POST /v1/platform/businesses/{id}/invoices/payments/
Idempotency-Key: payment-20240115-001
Behavior:
  • First request: Processes payment, returns 201 Created
  • Retry with same key: Returns cached response, returns 200 OK

Resources Supporting external_id

Businesses

business_data = {
    "external_id": f"customer-{customer.id}",  # Your customer ID
    "legal_name": "Example Ltd",
    "entity_type": "limited",
    "email": "[email protected]",
    "country": "GB",
    "currency": "GBP"
}

# Safe to retry - won't create duplicates
response = requests.post(
    "https://dev-backend.thredfi.com/v1/platform/businesses/",
    headers={"Authorization": f"Bearer {unscoped_token}"},
    json=business_data
)

if response.status_code == 200:
    print("Business already exists (idempotent)")
elif response.status_code == 201:
    print("Business created")

Invoices

invoice_data = {
    "external_id": f"invoice-{your_invoice.id}",
    "customer_id": customer_id,
    "invoice_number": "INV-001",
    "sent_at": "2024-01-15",
    "due_at": "2024-02-15",
    "currency": "GBP",
    "line_items": [...]
}

# Safe to retry
response = requests.post(
    f"{base_url}/v1/platform/businesses/{business_id}/invoices/",
    headers={"Authorization": f"Bearer {business_token}"},
    json=invoice_data
)

Bills

bill_data = {
    "external_id": f"bill-{your_bill.id}",
    "vendor_id": vendor_id,
    "bill_number": "BILL-001",
    "received_at": "2024-01-15",
    "due_at": "2024-02-15",
    "currency": "GBP",
    "line_items": [...]
}

# Idempotent
response = requests.post(
    f"{base_url}/v1/platform/businesses/{business_id}/bills/",
    headers={"Authorization": f"Bearer {business_token}"},
    json=bill_data
)

Customers & Vendors

customer_data = {
    "external_id": f"customer-{crm_customer.id}",
    "company_name": "Example Ltd",
    "email": "[email protected]"
}

# Idempotent
response = requests.post(
    f"{base_url}/v1/platform/businesses/{business_id}/customers/",
    headers={"Authorization": f"Bearer {business_token}"},
    json=customer_data
)

Operations Supporting Idempotency-Key Header

Invoice Payments

payment_data = {
    "amount_cents": 100000,
    "payment_date": "2024-01-20",
    "payment_method": "bank_transfer",
    "allocations": [
        {
            "invoice_id": invoice_id,
            "amount_cents": 100000
        }
    ]
}

# Use Idempotency-Key header
response = requests.post(
    f"{base_url}/v1/platform/businesses/{business_id}/invoices/payments/",
    headers={
        "Authorization": f"Bearer {business_token}",
        "Content-Type": "application/json",
        "Idempotency-Key": f"inv-payment-{invoice.id}-{datetime.now().date()}"
    },
    json=payment_data
)

Bill Payments

response = requests.post(
    f"{base_url}/v1/platform/businesses/{business_id}/bills/payments/",
    headers={
        "Idempotency-Key": f"bill-payment-{bill.id}-{payment.id}"
    },
    json=payment_data
)

Idempotency Key Format

Use a format that combines relevant identifiers:
  • Entity type + ID: invoice-123
  • Entity + date: payment-20240115-001
  • UUID: 550e8400-e29b-41d4-a716-446655440000
Good Examples:
invoice-payment-inv123-20240115
customer-onboard-cust456
bill-payment-bill789-pay001
Bad Examples:
random-string  # Not meaningful
timestamp-only # Can collide across resources

Idempotency Window

Idempotency keys are stored for 24 hours. After that, the same key can be reused for a new transaction.

Response Status Codes

StatusMeaningDescription
201CreatedResource created successfully (first request)
200OKResource already exists (idempotent retry)
The response body is identical in both cases.

Best Practices

Generate Stable IDs

# Good - stable, reproducible
external_id = f"{entity_type}-{your_db_id}"

# Bad - changes on retry
external_id = f"{entity_type}-{random.uuid4()}"

Check Response Status

response = create_business(business_data)

if response.status_code == 201:
    print("Business created")
    send_welcome_email()
elif response.status_code == 200:
    print("Business already exists (idempotent)")
    # Don't send duplicate emails
else:
    handle_error(response)

Use for All Creates

Always provide external_id or Idempotency-Key when creating resources:
# Always include external_id
invoice_data = {
    "external_id": f"invoice-{id}",  # ← Always include
    "invoice_number": "INV-001",
    ...
}

Handling Conflicts

If you retry with different data but same external_id:
# First request
{
  "external_id": "customer-123",
  "company_name": "Example Ltd"
}
# → 201 Created

# Retry with different data
{
  "external_id": "customer-123",
  "company_name": "Different Company"  # Changed!
}
# → 200 OK (returns ORIGINAL customer, ignores new data)
Idempotent retries with the same external_id always return the original resource, even if request data differs.To update a resource, use the update endpoint instead.

Example: Safe Invoice Creation

def create_invoice_safe(invoice_data, retry_count=3):
    """Create invoice with retry safety via external_id."""

    for attempt in range(retry_count):
        try:
            response = requests.post(
                f"{base_url}/v1/platform/businesses/{business_id}/invoices/",
                headers={
                    "Authorization": f"Bearer {business_token}",
                    "Content-Type": "application/json"
                },
                json={
                    "external_id": f"invoice-{invoice_data['id']}",  # Idempotent
                    **invoice_data
                },
                timeout=10
            )

            if response.status_code in [200, 201]:
                return response.json()

            elif response.status_code >= 500:
                # Server error - retry
                if attempt < retry_count - 1:
                    time.sleep(2 ** attempt)
                    continue

            else:
                # Client error - don't retry
                raise APIError(response.json())

        except requests.exceptions.Timeout:
            # Timeout - safe to retry because of external_id
            if attempt < retry_count - 1:
                time.sleep(2 ** attempt)
                continue
            raise

    raise MaxRetriesExceeded()

Next Steps