Appearance
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
- Before you raise a request, create a new key. A UUID is perfect:
b7e2c1d4-5a6f-4e3b-9c8d-1f2a3b4c5d6e. - Save it with your own record (your purchase, invoice or task) before you call Johari.
- Send it in the body as
idempotencyKey. - If the call fails with a network error or a timeout, send the same body with the same key again.
- Once you have a
200or a409, 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
| 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 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
POSTmust have anidempotencyKey: Create a Request, Respond to a Return and Add Files. Without one you get400. - 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,
filesincluded (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 gets400. - Tied to the request in the URL. For Respond to a Return and Add Files, the key is tied to the
requestIDin the URL as well as to the body. The same key and body sent for a different request gets400(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.