Skip to main content

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.

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": "contact@example.com",
    "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": "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

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

Error Handling

Handle errors during retries

Authentication

Token refresh during retries