> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thredfi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Prevent duplicate transactions with idempotent API calls

## 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:

```json theme={null}
{
  "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**

<Tip>
  The `external_id` should be **unique within your system** and **stable** (don't change it).
</Tip>

### 2. Idempotency-Key Header

For operations that don't support `external_id`, use the `Idempotency-Key` header:

```bash theme={null}
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

```python theme={null}
business_data = {
    "external_id": f"customer-{customer.id}",  # Your customer ID
    "legal_name": "Example Ltd",
    "entity_type": "limited",
    "email": "contact@example.com",
    "country": "GB",
    "base_currency": "GBP"
}

# Safe to retry - won't create duplicates
response = requests.post(
    "https://sandbox.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

```python theme={null}
invoice_data = {
    "external_id": f"invoice-{your_invoice.id}",
    "customer_id": customer_id,
    "invoice_number": "INV-001",
    "sent_at": "2026-06-11T10:00:00Z",
    "due_at": "2026-07-11T10:00:00Z",
    "line_items": [
        {
            "description": "Consulting services",
            "quantity": 1,
            "unit_price": 10000,
            "tax_code": "EXEMPT"
        }
    ]
}

# 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

```python theme={null}
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

```python theme={null}
customer_data = {
    "external_id": f"customer-{crm_customer.id}",
    "company_name": "Example Ltd",
    "email": "billing@example.com"
}

# 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

```python theme={null}
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

```python theme={null}
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

<Tip>
  Use a format that combines relevant identifiers:

  * Entity type + ID: `invoice-123`
  * Entity + date: `payment-20240115-001`
  * UUID: `550e8400-e29b-41d4-a716-446655440000`
</Tip>

**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

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

## Response Status Codes

| Status  | Meaning | Description                                   |
| ------- | ------- | --------------------------------------------- |
| **201** | Created | Resource created successfully (first request) |
| **200** | OK      | Resource already exists (idempotent retry)    |

The response body is identical in both cases.

## Best Practices

### Generate Stable IDs

```python theme={null}
# 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

```python theme={null}
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:

<CodeGroup>
  ```python Recommended theme={null}
  # Always include external_id
  invoice_data = {
      "external_id": f"invoice-{id}",  # ← Always include
      "invoice_number": "INV-001",
      ...
  }
  ```

  ```python Not Recommended theme={null}
  # Missing external_id - risky
  invoice_data = {
      "invoice_number": "INV-001",  # What if network fails?
      ...
  }
  ```
</CodeGroup>

## Handling Conflicts

If you retry with different data but same `external_id`:

```python theme={null}
# 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)
```

<Warning>
  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.
</Warning>

## Example: Safe Invoice Creation

```python theme={null}
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

<CardGroup cols={2}>
  <Card title="Error Handling" icon="triangle-exclamation" href="/implementation/error-handling">
    Handle errors during retries
  </Card>

  <Card title="Authentication" icon="key" href="/authentication">
    Token refresh during retries
  </Card>
</CardGroup>
