# Idempotency and retries

> Retry a request after a timeout without creating a payment or a refund twice.

Source: https://rais.app/docs/idempotency

Networks fail. A request can time out after Raïs has already created the payment, and a client that sends it again would create a second one. An **idempotency key** makes the retry safe: the same key with the same request returns the original answer, and nothing happens twice.

## Send a key

Add an `Idempotency-Key` header to `POST /payments` and `POST /payments/{id}/refunds`.


**Request**

```bash
curl https://api.rais.app/v1/payments \
  -H "Authorization: Bearer rais_sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{
  "amount": 500000,
  "reference": "order-1042"
}'
```


The key is any value of 255 printable ASCII characters or fewer. A random UUID works. A key derived from your own record, such as `order-1042`, gives you "one payment per order" for free. Generate the key **before** the first attempt and keep it with the order, so every retry reuses it.

## What happens when you repeat a request

| You send | Raïs answers |
| --- | --- |
| The same key and the same request | The **original answer**, with the original status code and the header `Idempotent-Replayed: true`. Nothing is created or refunded again. The content is the same; the keys of the JSON object may come in a different order. |
| The same key and a different request | `422` with `idempotency_key_reused`. |
| The same key while the first request is still running | `409` with `idempotency_in_progress` and a `Retry-After` header. Retry in a moment. |
| A key longer than 255 characters or with unusual characters | `400` with `invalid_idempotency_key`. |

A few details that matter:

- **Same request** means the same method, the same path and the same JSON body. Spacing and the order of keys do not matter.
- **Only successful requests are remembered.** A request that failed leaves nothing behind, so you can fix the problem and send it again with the same key.
- **Keys live 24 hours.** After that, the same key starts a new operation.
- **Sandbox and live are separate.** The same text used with a sandbox key and with a live key makes two different keys. Keys are also scoped to your store: two stores can use the same text without ever meeting.
- **A replay is a snapshot.** It returns the answer the first request got, for example a `pending` payment, even if the payment has been paid since. Retrieve the payment to see its current state.
- **A crashed request is not a duplicate.** If a request dies half-way, repeating it finds what it created instead of creating it again.

## Retry in code

Retry on network errors, `429`, `409 idempotency_in_progress`, `500` and `503`, with exponential backoff, and always with the same key.

~~~javascript
async function createPayment(body, idempotencyKey) {
  for (let attempt = 1; attempt <= 4; attempt++) {
    try {
      const response = await fetch("https://api.rais.app/v1/payments", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.RAIS_SECRET_KEY}`,
          "Content-Type": "application/json",
          "Idempotency-Key": idempotencyKey, // the same on every attempt
        },
        body: JSON.stringify(body),
      });
      const retryable = response.status === 429 || response.status === 409 || response.status >= 500;
      if (!retryable || attempt === 4) return response;

      const wait = Number(response.headers.get("Retry-After")) || 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, wait * 1000));
    } catch (error) {
      if (attempt === 4) throw error; // a network error: the request may or may not have arrived
      await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000));
    }
  }
}
~~~

## Reads and cancellations

`GET` requests are safe to repeat as they are. Cancelling a payment twice answers `409 payment_not_cancellable` the second time: treat a payment that is already `cancelled` as done.

## Webhooks are at least once

The other half of reliability is on your side of the line: Raïs delivers each webhook until you acknowledge it, so the same event can arrive twice. De-duplicate on the event `id`. See [Webhooks](https://rais.app/docs/webhooks.md).
