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

> ## Agent Instructions
> If you are an AI assistant operating Infinite for a user, read the skill first: https://docs.infinite.dev/agent-skill.md (setup per host, tool reference, rules for moving money, journeys).
> When operating Infinite for a user, use the MCP server. Sign in at https://bot.api.infinite.dev/mcp (production, acts as the user) or send `Authorization: Bearer <key>` to https://bot.api.infinite.dev/mcp/sse. Prefer its tools over raw HTTP; use REST only for steps no tool covers, and check `get_endpoint_schema` before guessing a request shape.
> Net-new Infinite Connect (org_connect) is operator-assisted: do not grant end users shared org_connect Auth Kit membership or the shared platform org API key — those credentials are org-wide on /v1. BYO org keys onboard under that customer's org id. Operator sessions should show org_connect in get_organization_info.
> The key decides the tools. Every organization key (`sk_sandbox_…` / `sk_live_…`) gets docs, onboarding, `get_account_balance`, and request logs for your own organization. BYO organizations add payees an agent will pay with `create_payee` (payable once ACTIVE). Platform Connect sessions instead get bound-customer payment lookups (`get_quote`, `get_transfer`, `list_transfers`, `list_account_balances`), payee drafts (`create_payee_draft` / `get_payee_draft`; screening only, never payable), and transfer drafts that wait for Infinite approval (`create_transfer_draft` / `get_transfer_draft`; never POST /v1/transfers). Where offered, platform `create_transfer` files a real transfer that Infinite holds in review before funding; it cannot be submitted from DRAFT.
> An agent key (`sk_sandbox_agent_…` / `sk_live_agent_…`) moves money within that agent's limits: `create_transfer` → `submit_transfer` → `wait_for_transfer`.
> On sandbox organization-key sessions, `sandbox_approve_customer` (makes the capabilities you pass READY, and related ones may turn READY too; `request_capabilities` is not needed), `sandbox_fund_account`, and `sandbox_complete_transfer` stand in for verification, funding, and settlement, so a whole flow runs from MCP. BYO keys can pass `customerId` on each call without binding; platform sessions must bind a customer first (`create_business_customer` auto-binds, or send `X-Infinite-On-Behalf-Of`).
> When searching documentation without a key, use the docs MCP at https://docs.infinite.dev/mcp.
> Start in sandbox (https://sandbox.api.infinite.dev). Do not use production unless the user explicitly asks.
> Confirm amount, currency, and payee with the user before any payment is submitted. Pass the invoice number as `reference` on `create_transfer`; the summary says what the payee will see.
> REST amounts are integer minor units (`"4000"` = 40.00 USD). MCP payment tools take exact decimal strings (`"40.00"`). Never mix them.
> Agent denials return `403 { denial: { code, message, window, limit, used, remaining, attemptedAmount, currency }, hint }` with decimal amounts; per-transfer denials omit `used` and `remaining`. Report them to the user; do not work around them.

# Idempotency

> Safely retry API requests without duplicating actions

## Overview

Idempotent requests allow you to safely retry API calls without the risk of performing the same operation twice. This is essential for building reliable integrations, especially when dealing with network failures or timeouts.

## How It Works

To make an idempotent request, include an `Idempotency-Key` header with a unique value. If the same key is used within 24 hours, the API returns the cached response from the first request instead of creating a new resource.

<CodeGroup>
  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.infinite.dev/v1/customers/individuals',
      headers={
          'Authorization': 'Bearer YOUR_API_KEY',
          'Idempotency-Key': 'unique-key-123',
          'Content-Type': 'application/json',
      },
      json={
          'customerName': 'John Doe',
          'email': 'john@example.com',
          'countryCode': 'US',
      },
  )
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    'https://api.infinite.dev/v1/customers/individuals',
    {
      method: 'POST',
      headers: {
        Authorization: 'Bearer YOUR_API_KEY',
        'Idempotency-Key': 'unique-key-123',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        customerName: 'John Doe',
        email: 'john@example.com',
        countryCode: 'US',
      }),
    },
  );
  ```

  ```bash CLI theme={null}
  export INFINITE_API_KEY=YOUR_API_KEY
  infinite api POST /v1/customers/individuals \
    --header 'Idempotency-Key: unique-key-123' \
    --body '{
    "customerName": "John Doe",
    "email": "john@example.com",
    "countryCode": "US"
  }'
  ```

  ```text MCP theme={null}
  Create an individual customer John Doe (john@example.com) in the US, using idempotency key unique-key-123.

  # Infinite MCP is read-only today — run the CLI tab to execute.
  ```

  ```bash cURL theme={null}
  curl https://api.infinite.dev/v1/customers/individuals \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Idempotency-Key: unique-key-123" \
    -H "Content-Type: application/json" \
    -d '{
      "customerName": "John Doe",
      "email": "john@example.com",
      "countryCode": "US"
    }'
  ```
</CodeGroup>

### First Request

* Processes the request normally
* Creates the resource
* Stores the request and response for 24 hours
* Returns the newly created resource

### Subsequent Requests (Same Key)

* Detects duplicate key
* Returns the cached response immediately
* No new resource is created
* No side effects occur

## Supported Endpoints

Idempotency is supported on the following resource creation endpoints:

* **Customers**: `POST /customers/individuals`, `POST /customers/businesses`
* **Associated Entities**: `POST /customers/:customerId/associated_individuals`, `POST /customers/:customerId/associated_businesses`
* **Transfers**: `POST /transfers`
* **Quotes**: `POST /quotes`
* **FX Quotes**: `POST /fx/quotes`
* **Balance Accounts**: `POST /balance-accounts`
* **Fixed Routes**: `POST /fixed-routes/virtual-bank-accounts`, `POST /fixed-routes/virtual-wallets`

Endpoints not listed here ignore the `Idempotency-Key` header — retrying a request against them may create duplicate resources.

## Generating Idempotency Keys

Idempotency keys should be:

* **High-entropy**: Use UUIDs — do not derive keys from guessable values like sequential order numbers
* **Stable across retries**: Generate the key once per operation, persist it, and reuse the same value for every retry of that operation
* **Short-lived**: Keys expire after 24 hours

### Best Practices

Generate a UUID when the operation is first attempted and store it alongside the operation with an atomic get-or-create, then reuse that stored value for every retry:

```javascript theme={null}
// Good: compare-and-set so concurrent first attempts converge on one key
const candidateKey = crypto.randomUUID();
const result = await db.query(
  `UPDATE orders
   SET idempotency_key = COALESCE(idempotency_key, $1)
   WHERE id = $2
   RETURNING idempotency_key AS "idempotencyKey"`,
  [candidateKey, orderId]
);
const idempotencyKey = result.rows[0].idempotencyKey;
```

A plain read-then-write can lose under concurrency: two workers both see no key, generate different UUIDs, and then retry with different keys.

Serialize retries of the same operation: wait for a response or timeout before sending another request with the same key. Do not fire overlapping concurrent requests with one key.

Avoid these patterns:

```javascript theme={null}
// Bad: including a timestamp makes every retry a new key
const badTimestampKey = `create-customer-${customerId}-${Date.now()}`;

// Bad: guessable, low-entropy keys risk collisions
const badGuessableKey = `create-transfer-${orderNumber}`;

// Bad: reusing the same key for different operations
const badSharedKey = 'my-key';
```

## Error Handling

If the original request failed, subsequent requests with the same key will return the same error. This prevents partial state issues.

<CodeGroup>
  ```python Python theme={null}
  import requests

  headers = {
      'Authorization': 'Bearer YOUR_API_KEY',
      'Idempotency-Key': 'retry-key-456',
      'Content-Type': 'application/json',
  }

  # First request fails (missing required fields)
  requests.post(
      'https://api.infinite.dev/v1/customers/individuals',
      headers=headers,
      json={'email': 'invalid'},
  )
  # Response: 400 Bad Request

  # Retry with same key — cached error is returned even if body changed
  requests.post(
      'https://api.infinite.dev/v1/customers/individuals',
      headers=headers,
      json={'name': 'Fixed', 'email': 'valid@example.com'},
  )
  # Response: 400 Bad Request (same error cached)
  ```

  ```typescript TypeScript theme={null}
  // First request fails (missing required fields)
  await fetch('https://api.infinite.dev/v1/customers/individuals', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer YOUR_API_KEY',
      'Idempotency-Key': 'retry-key-456',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email: 'invalid' }),
  });
  // Response: 400 Bad Request

  // Retry with same key — cached error is returned even if body changed
  await fetch('https://api.infinite.dev/v1/customers/individuals', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer YOUR_API_KEY',
      'Idempotency-Key': 'retry-key-456',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ name: 'Fixed', email: 'valid@example.com' }),
  });
  // Response: 400 Bad Request (same error cached)
  ```

  ```bash CLI theme={null}
  export INFINITE_API_KEY=YOUR_API_KEY

  # First request fails
  infinite api POST /v1/customers/individuals \
    --header 'Idempotency-Key: retry-key-456' \
    --body '{"email": "invalid"}'
  # Response: 400 Bad Request

  # Retry with same key — cached error is returned
  infinite api POST /v1/customers/individuals \
    --header 'Idempotency-Key: retry-key-456' \
    --body '{"name": "Fixed", "email": "valid@example.com"}'
  # Response: 400 Bad Request (same error cached)
  ```

  ```text MCP theme={null}
  Create an individual customer with idempotency key retry-key-456 and an invalid body, then retry with the same key — the cached 400 comes back.

  # Infinite MCP is read-only today — run the CLI tab to execute.
  ```

  ```bash cURL theme={null}
  # First request fails
  curl -X POST https://api.infinite.dev/v1/customers/individuals \
    -H "Idempotency-Key: retry-key-456" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"email": "invalid"}'

  # Response: 400 Bad Request

  # Retry with same key (body may differ — cached error is returned)
  curl -X POST https://api.infinite.dev/v1/customers/individuals \
    -H "Idempotency-Key: retry-key-456" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"name": "Fixed", "email": "valid@example.com"}'

  # Response: 400 Bad Request (same error cached)
  ```
</CodeGroup>

To fix the request, use a **new idempotency key**.

## Key Expiration

Idempotency keys expire after **24 hours**. After expiration:

* The key can be reused for a new operation
* Previous responses are no longer cached

## When to Use

### ✅ Use Idempotency For:

* Network retry logic
* Webhook handlers (use webhook event ID as key)
* User-triggered actions that could be double-clicked
* Automated scheduled jobs
* Any situation where duplicate requests are possible

### ❌ Don't Use For:

* Read operations (GET requests) - they're naturally idempotent
* Update operations (PATCH/PUT) - these are typically idempotent by design
* Operations you want to repeat intentionally

## Implementation Examples

### JavaScript/TypeScript

```typescript theme={null}
import { v4 as uuidv4 } from 'uuid';

async function createCustomer(data: CustomerData) {
  const idempotencyKey = uuidv4();

  try {
    const response = await fetch(
      'https://api.infinite.dev/v1/customers/individuals',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${API_KEY}`,
          'Idempotency-Key': idempotencyKey,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
      },
    );

    return await response.json();
  } catch (error) {
    // Safe to retry with same key
    return await retryWithSameKey(idempotencyKey, data);
  }
}
```

### Python

```python theme={null}
import uuid
import requests

def create_customer(data):
    idempotency_key = str(uuid.uuid4())

    headers = {
        'Authorization': f'Bearer {API_KEY}',
        'Idempotency-Key': idempotency_key,
        'Content-Type': 'application/json',
    }

    try:
        response = requests.post(
            'https://api.infinite.dev/v1/customers/individuals',
            headers=headers,
            json=data
        )
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        # Safe to retry with same key
        return retry_with_same_key(idempotency_key, data)
```

### Webhook Handler Example

```typescript theme={null}
// Use webhook event ID as idempotency key
async function handleWebhook(event: WebhookEvent) {
  const idempotencyKey = event.id; // Webhook event ID

  await createTransfer(
    {
      // ... transfer data
    },
    idempotencyKey
  );
}
```

## Technical Details

* **Storage**: Idempotency keys are stored in the database
* **TTL**: 24 hours from first request
* **Scope**: Keys are scoped by operation type. Related routes that create the same resource type share one cache (for example `POST /customers/individuals` and `POST /customers/businesses`; similarly the two associated-entity create routes). Always use a unique high-entropy UUID per distinct operation — never guessable or shared values.
* **Cleanup**: Expired keys are automatically cleaned up

## Limitations

* Do not reuse a key across different operations — reusing one can return the first cached response
* Related creation routes that create the same resource type share one cache; reuse a key only when retrying that same operation
* Do not send overlapping concurrent requests with the same key — wait for a response or timeout, then retry
* Maximum key length: 255 characters
* Only POST operations support idempotency (creation endpoints)


## Related topics

- [Introduction](/api-reference/introduction.md)
- [Webhooks](/guides/webhooks.md)
- [Customers](/concepts/customers.md)
- [Create a quote](/api-reference/quotes/create-a-quote.md)
- [Draw USDC credit into Solana treasury](/api-reference/liquidity/draw-usdc-credit-into-solana-treasury.md)
