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

> ## Agent Instructions
> When building application code, use `@naturalpay/sdk` for TypeScript or `naturalpay` for Python.
> When operating Natural directly for a user, use the payments MCP at `https://mcp.natural.com`.
> When searching Natural documentation, use the docs MCP at `https://docs.natural.com/mcp`.
> Use the Natural CLI for terminal and CI automation.
> Use REST only for unsupported languages or when the user explicitly requests raw HTTP.
> Start integration development and testing in the sandbox. Do not use production unless the user explicitly requests it.
> For sandbox requests that require `customerPartyId`, list customers first. Reuse only the intended fixture; otherwise create a customer.
> REST and SDK amounts use integer minor units. Payments MCP amounts use decimal strings with a required currency code.

# Idempotency

> Ensuring safety when retrying a mutation

Idempotency ensures retries are safe for mutating requests. Retrying the same request with the same `Idempotency-Key` never executes side effects twice.

## How it works

Include an `Idempotency-Key` header on every request to an endpoint that requires one. Natural records the key alongside the outcome of the request, and a later request with the same key returns that recorded outcome instead of running the operation again.

```python theme={null}
response = await client.post(
    "https://api.natural.com/payments",
    headers={
        "Authorization": "Bearer sk_ntl_prod_abc123...",
        "Idempotency-Key": str(uuid.uuid4())
    },
    json={...}
)
```

On subsequent requests with the same key:

* **Same key + same request + finished** -> Replays the recorded response.
* **Same key + different request** -> Returns `409 conflict`.
* **Same key + original still running** -> Returns `409 conflict`.
* **Different key** -> Treated as a new mutation attempt.

Replayed responses include `X-Idempotency-Replayed: true`. That header is the only thing distinguishing a replay from a fresh execution, so check it if you need to tell them apart.

## Endpoints that require a key

The `Idempotency-Key` header is required on the endpoints below. Each one is a mutating operation whose accidental repetition would be visible to you or your customers.

| Endpoint | Operation | Resource |
| - | - | - |
| `POST /agents` | Create agent | Agents |
| `DELETE /agents/{agentId}` | Delete agent | Agents |
| `PATCH /agents/{agentId}` | Update agent | Agents |
| `DELETE /customers/{customerId}` | Disconnect customer | Customers |
| `POST /payments` | Create payment | Payments |
| `POST /payments/{paymentId}/cancel` | Cancel payment | Payments |
| `POST /payment-intents` | Create payment intent | Payment Intents |
| `PATCH /payment-intents/{paymentIntentId}` | Update payment intent | Payment Intents |
| `POST /payment-intents/{paymentIntentId}/cancel` | Cancel payment intent | Payment Intents |
| `POST /voice/sessions` | Create voice session | Voice |
| `POST /refunds` | Create refund | Refunds |
| `POST /chargebacks/{chargebackId}/accept` | Accept chargeback | Chargebacks |
| `PATCH /chargebacks/{chargebackId}/evidence` | Update chargeback evidence | Chargebacks |
| `POST /chargebacks/{chargebackId}/files` | Upload chargeback file | Chargebacks |
| `POST /chargebacks/{chargebackId}/submit` | Submit chargeback evidence | Chargebacks |
| `POST /transfers/deposit` | Initiate deposit | Transfers |
| `POST /transfers/internal` | Initiate internal transfer | Transfers |
| `POST /transfers/withdraw` | Initiate withdrawal | Transfers |
| `POST /payment-requests` | Create payment request | PaymentRequests |
| `POST /payment-requests/{paymentRequestId}/cancel` | Cancel payment request | PaymentRequests |
| `POST /payment-requests/{paymentRequestId}/decline` | Decline payment request | PaymentRequests |
| `POST /payment-requests/{paymentRequestId}/fulfill` | Fulfill payment request | PaymentRequests |
| `POST /approvals/{approvalId}/approve` | Approve payment or transfer | Approvals |
| `POST /approvals/{approvalId}/deny` | Deny payment or transfer | Approvals |
| `DELETE /party-invitations/{invitationId}` | Revoke party invitation | Invitations |
| `PATCH /parties/{partyId}/branding` | Update party branding | Parties |
| `DELETE /parties/me/members/{userId}` | Remove party member | Parties |
| `POST /wallets` | Create wallet | Wallets |
| `PATCH /wallets/{walletId}` | Update wallet | Wallets |
| `POST /wallets/{walletId}/agents` | Grant agent access to wallet | Wallets |
| `DELETE /wallets/{walletId}/agents/{agentId}` | Detach agent from wallet | Wallets |
| `POST /wallets/{walletId}/agents/{agentId}/default` | Set agent default wallet | Wallets |
| `POST /wallets/{walletId}/default` | Set default wallet | Wallets |
| `POST /external-parties` | Create external party | External Parties |
| `POST /external-party-accounts` | Store banking details for an external party | External Party Accounts |
| `POST /ach` | Create ACH payment | ACH |
| `POST /ach/{achId}/cancel` | Cancel ACH payment | ACH |
| `POST /realtime` | Create realtime payment | Realtime |
| `POST /realtime/{realtimeId}/cancel` | Cancel realtime payment | Realtime |
| `POST /wire` | Create wire payment | Wire |
| `POST /wire/{wireId}/cancel` | Cancel wire payment | Wire |
| `DELETE /api-keys/{keyId}` | Revoke API key | API Keys |
| `DELETE /agent-keys/{keyId}` | Revoke agent key | Agent Keys |
| `POST /agent-keys/{keyId}/rotate` | Rotate agent key | Agent Keys |
| `POST /agentic-payments/purchases` | Create agentic purchase | Agent Tokens |
| `POST /webhooks` | Create webhook | Webhooks |
| `DELETE /webhooks/{webhookId}` | Delete webhook | Webhooks |
| `PATCH /webhooks/{webhookId}` | Update webhook | Webhooks |
| `POST /webhooks/{webhookId}/rotate-secret` | Rotate webhook signing secret | Webhooks |
| `POST /webhooks/{webhookId}/events/{eventId}/redeliver` | Redeliver event | Events |
| `POST /simulations/card-payments/{cardPaymentId}/settle` | Sandbox: settle a card payment | Simulations |
| `POST /simulations/customer-invitations/{invitationId}/accept` | Accept customer invitation as test customer | Simulations |
| `POST /simulations/customer-invitations/{invitationId}/decline` | Decline customer invitation as test customer | Simulations |
| `POST /simulations/customers/{customerId}/agents/{agentId}/revoke` | Revoke agent access as test customer | Simulations |
| `POST /simulations/customers/{customerId}/disconnect` | Disconnect test customer | Simulations |
| `POST /simulations/customers/{customerId}/transfers/deposit` | Fund test customer | Simulations |
| `POST /simulations/external-accounts/link` | Link test bank account | Simulations |
| `POST /simulations/invite-customer` | Invite test customer | Simulations |
| `POST /simulations/payment-requests/{paymentRequestId}/decline` | Decline payment request as test payer | Simulations |
| `POST /simulations/payment-requests/{paymentRequestId}/fulfill` | Fulfill payment request as test payer | Simulations |
| `POST /simulations/realtime/{realtimeId}/fail` | Sandbox: fail a realtime payment | Simulations |
| `POST /simulations/realtime/{realtimeId}/settle` | Sandbox: settle a realtime payment | Simulations |
| `POST /simulations/wire/{wireId}/fail` | Sandbox: fail a wire payment | Simulations |
| `POST /simulations/wire/{wireId}/settle` | Sandbox: settle a wire payment | Simulations |
| `POST /plans` | Create plan | Plans |
| `POST /plans/{planId}/cancel` | Cancel plan | Plans |
| `POST /plans/{planId}/payments` | Charge plan payment | Plans |
| `POST /plans/{planId}/resume` | Resume plan | Plans |
| `POST /plans/{planId}/skip` | Skip plan payment | Plans |

Omitting the header on any of these returns `400` with the error code `invalid_value`. A key longer than 255 characters is rejected the same way. Endpoints not listed here ignore the header.

## Keys

A key must be unique per logical operation and stable across every retry of that operation. Natural binds the key to the meaningful contents of the request it first arrives with, so the same key must always mean the same request.

* Use a UUIDv4/UUIDv7, or any unique string of up to 255 characters.
* Generate the key **once**, when the operation is first attempted, and reuse it for every retry of that attempt, including retries after a timeout.
* Generate a **new** key only when the user or system starts a genuinely new operation.
* Never derive a key from anything that changes between retries (a timestamp, an attempt counter, a per-request random value). A key that changes per retry provides no protection at all.
* Don't put sensitive data in keys.

Keys are scoped to the party the request acts on. Two parties can use the same key string without interfering, and a key is never shared across parties.

## Replay window

A record is created when the request starts, and is finalized once the operation reaches a terminal outcome: success or failure. **The record then expires 48 hours after it finished**, not 48 hours after the request arrived.

Within those 48 hours, the same key replays the recorded outcome. Once they elapse the key is forgotten, and reusing it starts an entirely new request, re-running the side effect. Treat 48 hours as the window in which a retry is guaranteed safe, not as a deduplication guarantee for the life of the key.

A record for an operation that is still running does not expire on its own. If an operation never reaches a terminal outcome, its key stays reserved and requests reusing it keep returning `409 conflict`. Contact support if a key stays conflicted.

## Conflicts

Both conflict cases return `409` with the error code `conflict`:

```json theme={null}
{
  "errors": [
    {
      "code": "conflict",
      "detail": "The request conflicts with the current resource state.",
      "status": "409",
      "meta": {
        "supportId": "req_a1b2c3d4e5f6"
      }
    }
  ]
}
```

The response body is identical in both cases, so tell them apart by what your own client did:

* **You retried an identical request.** The original is still running. Wait, then retry the same key with exponential backoff.
* **You changed the request but reused the key.** This is a client bug: a key is bound to the first request sent under it. Use a fresh key for the new operation.

Include `meta.supportId` when contacting support about a conflict; the underlying reason is recorded against that ID.

## Failures are replayed too

Failures are recorded exactly like successes. When an operation fails terminally, the same key replays that same error (identical code, status, and detail) for the rest of the window. Retrying a recorded failure with the same key does not re-run it.

Responses that do not reflect a decision about your request are not recorded: `408` (timeout), `429` (rate limited), and a `503` caused by a transient outage of one of Natural's services. Retrying any of them with the same key genuinely re-runs the operation. A `503` that is a decision, such as a balance check that could not be completed, is recorded like any other error. One `400` is also not recorded: `wire.beneficiary_address_required` on `POST /wire`, so after you add the address the same key re-runs and succeeds.

## When to retry

| Situation | Action |
| - | - |
| Network failure or timeout with no response | Retry with the **same** key, exponential backoff. If the original landed you get its result; if it never did, it runs now |
| `408`, `429`, or a transient `503` | Retry with the **same** key, exponential backoff. The record is released, so the retry re-runs |
| `500` or `502` | Retry with the **same** key. You get the recorded error replayed, or a `409` while the original resolves. A transient database error is not recorded, so that retry re-runs |
| `409 conflict` after an identical retry | The original is still running. Wait, then retry with the **same** key |
| `409 conflict` after changing the request | Client bug. Use a **new** key for the new operation |
| Other `4xx` | Don't retry the same key; it replays the error. Fix the request, then send it under a **new** key. Exception: `wire.beneficiary_address_required` is not recorded, so fix the address and retry the **same** key |

<Warning>
  Don't switch to a new key to force a failed operation to re-run. A new key is a new operation: if
  the original moved money before failing, a fresh key can move it a second time. Check the
  resource's status first, and only re-attempt under a new key once you have confirmed the original
  did not take effect.
</Warning>

## Related

* [Error Handling](/api-reference/errors/error-handling): Handling conflicts and other errors


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.