---
url: /guide/idempotency.md
---
# Idempotency

Networks fail. Sometimes you send a request and never get the answer: the connection drops, or your server times out. You don't know if Johari created the payment request or not.

If you just send it again, you might create **two payment requests for the same thing**, and someone might pay twice.

The `idempotencyKey` solves this. It is a unique ID **you** choose for each payment request. Johari remembers it. If the same key arrives again, Johari does not create a second request.

All three calls that change something use `idempotencyKey` the same way: [Create a Request](/api/create-request), [Respond to a Return](/api/respond-to-return) and [Add Files](/api/add-files). A retry never answers a return twice or adds the same files twice.

## The rule

> **One payment request = one idempotency key.** Keep the key until you get a clear answer. Use a new key only for a new payment request.
>
> The same goes for the other two writes: a new key for every new response to a return, and for every new upload of files. Reuse a key only to retry that same call.

## How to use it

1. Before you raise a request, create a new key. A UUID is perfect: `b7e2c1d4-5a6f-4e3b-9c8d-1f2a3b4c5d6e`.
2. **Save it** with your own record (your purchase, invoice or task) **before** you call Johari.
3. Send it in the body as `idempotencyKey`.
4. If the call fails with a network error or a timeout, send **the same body with the same key** again.
5. Once you have a `200` or a `409`, you are done. Never send that key again for a different request.

::: tip Use your own ID if you have one
If every payment request in your system already has a unique ID (for example `PO-2026-00123-PAY-1`), you can use that as the key. Any string up to 255 characters works.
:::

## What Johari answers

| Situation | HTTP | `data` | What to do |
|---|---|---|---|
| New key | `200` | The result. For a create, the new request | Save `requestID` |
| Key already used by a call that **succeeded** | `409` | What that call returned. For a create, the request it created | Treat it as success. Save `requestID` from `data` |
| Key already in use by a call that is **still running** | `409` | `null` | Wait a few seconds and send it again |
| Key already used for a **different** call (any field differs, or another `requestID` in the URL) | `400` | The error text | A bug in your code: every new create, response or upload needs a new key |
| Key used by a call that **failed** (e.g. `400`) | (as normal) | | Fix the data and send it again **with the same key**. A failed call does not use up the key |

### The `409` after success

```json
{
  "status": "error",
  "data": {
    "requestID": "6650f1a2b3c4d5e6f7a8b9e1",
    "requestNumber": "1042",
    "status": "pending"
  },
  "message": "A request with this idempotencyKey has already been processed"
}
```

This is **good news**: your first call worked. The `data` is exactly what the first call returned, so you can carry on as if you got the `200`. The same holds for [Respond to a Return](/api/respond-to-return) and [Add Files](/api/add-files): `data` is what their first call returned.

::: warning The `status` in a `409` may be out of date
`data.status` is the status **when the first call ran** (for a create, when the request was created), not now. If the request has been approved or rejected since then, the `409` still shows the old status. Use `requestID` from it, and call [Get a Request](/api/get-request) for the current status.
:::

### The `409` while still running

```json
{
  "status": "error",
  "data": null,
  "message": "A request with this idempotencyKey is still being processed"
}
```

Two copies of your call arrived at the same moment. One is doing the work (for a create, creating the request), the other was stopped. Wait a few seconds and retry: you will get the `200`-style `409` above.

## Example: a safe retry loop

::: code-group

```js [Node.js]
const crypto = require("crypto");

// callJohari is the function from the Signing page.
async function raiseRequest(fields) {
  // In a real system, save this key with your own record before the first try.
  const idempotencyKey = crypto.randomUUID();
  const payload = { ...fields, idempotencyKey };

  for (let attempt = 1; attempt <= 5; attempt++) {
    try {
      const result = await callJohari("POST", "/api/requests", payload);

      if (result.httpStatus === 200) return result.data; // created
      if (result.httpStatus === 409 && result.data) return result.data; // already created earlier
      if (result.httpStatus === 409) {
        await new Promise((r) => setTimeout(r, 2000)); // still being processed
        continue;
      }
      if (result.httpStatus === 429) {
        // Over the per-minute limit: it resets within a minute
        await new Promise((r) => setTimeout(r, 60000));
        continue;
      }
      if (result.httpStatus >= 500) {
        await new Promise((r) => setTimeout(r, 2000 * attempt));
        continue;
      }
      // 400, 401, 403: retrying the same data will not help
      throw new Error(`${result.httpStatus}: ${result.message} ${JSON.stringify(result.data)}`);
    } catch (error) {
      if (error.message.match(/^\d{3}:/)) throw error;
      // Network error or timeout: retry with the SAME payload and key
      await new Promise((r) => setTimeout(r, 2000 * attempt));
    }
  }
  throw new Error("Could not reach Johari; retry later with the same idempotencyKey");
}
```

```python [Python]
import time
import uuid

# call_johari is the function from the Signing page.
def raise_request(fields):
    # In a real system, save this key with your own record before the first try.
    payload = {**fields, "idempotencyKey": str(uuid.uuid4())}

    for attempt in range(1, 6):
        try:
            result = call_johari("POST", "/api/requests", payload)
        except Exception:
            # Network error or timeout: retry with the SAME payload and key
            time.sleep(2 * attempt)
            continue

        status = result["httpStatus"]
        if status == 200:
            return result["data"]  # created
        if status == 409 and result["data"]:
            return result["data"]  # already created earlier
        if status == 429:
            time.sleep(60)  # over the per-minute limit: it resets within a minute
            continue
        if status == 409 or status >= 500:
            time.sleep(2 * attempt)  # still being processed, or a server error
            continue
        # 400, 401, 403: retrying the same data will not help
        raise RuntimeError(f"{status}: {result['message']} {result['data']}")

    raise RuntimeError("Could not reach Johari; retry later with the same idempotencyKey")
```

:::

## Details

* **Required.** Every `POST` must have an `idempotencyKey`: [Create a Request](/api/create-request), [Respond to a Return](/api/respond-to-return) and [Add Files](/api/add-files). Without one you get `400`.
* **Checked first.** The key is checked before any other field, and before the request in the URL is looked up.
* **Tied to the body.** A retry must send the same body, `files` included (the same names, types and base64 content). The order of the fields does not matter, but every value must be the same. The same key with any different value gets `400`.
* **Tied to the request in the URL.** For Respond to a Return and Add Files, the key is tied to the `requestID` in the URL as well as to the body. The same key and body sent for a different request gets `400` (`This idempotencyKey was already used for a different request`).
* **One key, one call.** Keys are shared by all three writes. A key used to create a request cannot be used to respond or to add files, and the other way round.
* **A string of 1 to 255 characters.** A number or an object gets `400`.
* **Scoped to your API key.** Your keys can never clash with another system's keys.
* **Kept forever.** A key that worked can never be used for anything else, no matter how much later it arrives.
* **Signed.** The key is in the body, and the body is signed, so nobody can change it on the way.

## Common mistakes

::: danger Making a new key on every retry
If each retry gets a new key, Johari sees each one as a new request and creates duplicates. The key must be created **once**, before the first try, and reused for every retry of that request.
:::

::: danger Reusing a key for a different request
If you send a new, different request with an old key, you get `400` (`This idempotencyKey was already used for a different request`) and **no request is created**. Every new payment request needs a new key.

The same goes for responses and uploads. A request can be returned more than once: answer each return with a new key. Sending the same key and body for another request gets the same `400`.

On a retry, send exactly the same values, files included. Even a change like `150000` to `"150,000"` counts as a different request.
:::

::: danger Treating `409` as a failure
A `409` with `data` means your first call **worked**: the request was created, the return answered, or the files added. For a create, save the `requestID` from `data` and carry on.
:::

## Related pages

* [Create a Request](/api/create-request)
* [Respond to a Return](/api/respond-to-return)
* [Add Files](/api/add-files)
* [Errors](/guide/errors)
