Skip to content

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, Respond to a Return and 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.

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 ​

SituationHTTPdataWhat to do
New key200The result. For a create, the new requestSave requestID
Key already used by a call that succeeded409What that call returned. For a create, the request it createdTreat it as success. Save requestID from data
Key already in use by a call that is still running409nullWait a few seconds and send it again
Key already used for a different call (any field differs, or another requestID in the URL)400The error textA 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 and Add Files: data is what their first call returned.

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 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 ​

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
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, Respond to a Return and 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 ​

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.

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.

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.