# Idempotency

> Prevent duplicate financial operations with idempotent API requests.

Nexpay supports idempotent requests for financial mutation endpoints, allowing you to safely retry requests without the risk of performing the same operation twice. This is especially important for payment operations where network issues or timeouts may leave you uncertain whether a request was processed.

---

## How it works

Financial mutation endpoints support idempotency — most importantly **submitting a payment intent** (`POST /v2/payment-intents/{id}/submit`). Nexpay uses two mechanisms to prevent duplicates:

1. **Idempotency key** (recommended) -- You provide an explicit key via the `Idempotency-Key` header.
2. **Automatic fingerprinting** -- If no key is provided, Nexpay automatically generates a fingerprint based on the request body to detect identical requests.

When an idempotency key is provided, it takes precedence over automatic fingerprinting.

---

## Using the Idempotency-Key header

Include the `Idempotency-Key` header in your request with a unique value (e.g. a UUID) that identifies the intended operation:

**cURL**

```bash
curl -X POST 'https://api.nexpay.com.au/v2/payment-intents/507f1f77bcf86cd799439011/submit' \
  -H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
  -H 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000'
```

**JavaScript**

```javascript
try {
  const intentId = '507f1f77bcf86cd799439011';
  const response = await fetch(`https://api.nexpay.com.au/v2/payment-intents/${intentId}/submit`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': 'nxp_ck_your-client-id:nxp_sk_your-secret',
      // Derive the key from a stable upstream id (e.g. your order id) and reuse it across retries.
      'Idempotency-Key': '550e8400-e29b-41d4-a716-446655440000',
    },
  });

  if (response.ok) {
    const { data: intent } = await response.json();

    console.log(intent);
  } else {
    throw new Error(`Request failed with status: ${response.status}`);
  }
} catch (error) {
  console.error(error);
}
```

**PHP (Laravel)**

```php
use Illuminate\Support\Facades\Http;

$intentId = '507f1f77bcf86cd799439011';

$response = Http::withHeaders([
    'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
    // Derive the key from a stable upstream id (e.g. your order id) and reuse it across retries.
    'Idempotency-Key' => '550e8400-e29b-41d4-a716-446655440000',
])->post("https://api.nexpay.com.au/v2/payment-intents/{$intentId}/submit");

if ($response->successful()) {
    $intent = $response->json('data');

    print_r($intent);
} else {
    throw new Exception("Request failed with status: {$response->status()}");
}
```

> **Note — Submit is idempotent; Create is not**
>
> `POST /v2/payment-intents/{id}/submit` is the idempotent call. `POST /v2/payment-intents` (Create) is **not** — a retried Create produces a second draft intent. See [Manual payment with splits](/docs/guides/payment-intents-manual.md#idempotency).

---

## Behavior

| Scenario | Result |
| --- | --- |
| First request with a given key | Request is processed normally and the response is cached. |
| Retry with the same key and the same body | The cached response from the original request is returned. The operation is not executed again. |
| Same key but different request body | HTTP `422 Unprocessable Entity` is returned. This prevents replay attacks where a previously used key is reused with a modified amount or other parameters. |
| No `Idempotency-Key` header provided | Nexpay automatically detects duplicate requests by fingerprinting the request body. Identical requests within the deduplication window are treated as retries. |

> **Warning — Key reuse with different body**
>
> If you send a request with the same `Idempotency-Key` but a different request body, Nexpay will reject it with HTTP `422`. Always generate a new key for each unique operation.

---

## Best practices

* **Generate a unique key per operation** -- Use a UUID or another unique identifier for each distinct payment. Do not reuse keys across different operations.
* **Store keys alongside your records** -- Save the idempotency key with the corresponding record in your system so you can reliably retry with the same key if needed.
* **Retry safely on network errors** -- If a request times out or you receive a network error, retry with the same `Idempotency-Key` to safely determine whether the original request was processed.
* **Keys expire after 24 hours** -- Idempotency keys are valid for 24 hours after the first request. After expiry, the same key may be reused for a new operation; an expired key replayed within the same 24h window returns the originally cached response.
* **Which errors bind the key, which release it** — Some errors lock the key to a failed-response replay; others leave the key un-engaged so a retry is safe. Quick reference:

  | First-call outcome | Same key on retry |
  | --- | --- |
  | 2xx | Returns the original response (cached). |
  | 4xx validation error | Bound to the error response — mint a new key for any corrected payload. |
  | `[QOT0001]` (expired quote) | Bound to the error — mint a new intent and a new key. See [recovery](/docs/guides/payment-intents-manual.md#step-6-submit). |
  | `[PAY0003]` from the connector | **Not** engaged — retry with the SAME key after the dedup window. |
  | 5xx / network timeout | State unknown — either replay with the same key or `GET` the resource first to confirm state. |
  | 429 | **Not** engaged — retry with the same key after `Retry-After`. |
