Idempotency & rate limits
A network timeout does not tell you whether the borrower was debited — the idempotency key is how you find out without debiting them twice.
A network timeout does not tell you whether the borrower was debited. The idempotency key is how you find out without debiting them twice.
Idempotency-Key is optional on Lending Core mutations — 1 to 50 characters. Omit it and the server generates a fresh key per attempt, so nothing correlates one attempt with the next. Send your own key on any mutation you might retry, and always on money movement.
- The key matches a completed attemptstored
- The payload fingerprint is identicalamount, instrument, rail, date
- A second debit is creatednever
One key per logical intent
curl -X POST "$BASE/v1/loans/7204/payments" \
-H "Authorization: Bearer $TOKEN" \
-H "LendEasy-Tenant: demo-lender" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: harbor-installment-1-maya" \
-d '{
"instrumentId": 704,
"amount": "517.14",
"currency": "USD",
"rail": "ACH",
"effectiveDate": "2026-09-12",
"source": "BORROWER"
}'
If the connection drops after submission, resend the same method, path, body, tenant, and key. While the original is still executing, the duplicate returns 409 with Retry-After; wait that long and resend it.
The same key with a different amount is refused
The four money-movement creates — payment create, external payment record, funding create, and external funding record — bind the key to a fingerprint of the payload. Reusing a key with a different amount, instrument, or anything else returns 422:
{
"developerMessage": "Idempotency-Key was already used with a different payload.",
"httpStatusCode": "422",
"defaultUserMessage": "The request could not be applied.",
"userMessageGlobalisationCode": "validation.msg.idempotency.payload.mismatch",
"errors": []
}
Minting a random key to “fix” the mismatch orders a second operation.
Keys end up in logs, so keep them boring
Keep keys opaque: at most 50 characters, letters, digits, and simple separators, encoding your workflow intent without personal or secret data:
good: pay-hbr-inst-1-2026-09-12
good: funding-hbr-disburse-v1
bad: maya-chen-ssn-4821
bad: random-key-created-on-every-retry
externalId is different
externalId ties a lasting resource back to your system. Idempotency-Key deduplicates one HTTP mutation. Use both where offered:
- application external ID:
origination-HBR-2026-001; - create-request key:
create-origination-HBR-2026-001.
Changing an idempotency key does not make a duplicate externalId valid.
An unknown outcome is not a failure
| Situation | Safe behavior |
|---|---|
| Client timeout before response | Retry with the identical key and body. |
409 with Retry-After |
Still in flight; wait, then resend the same key and body. |
409 state conflict |
Read current state; decide whether a new intent is required. |
| Provider outcome unknown | Read the LendEasy intent; do not invoke the provider directly. |
422 fingerprint mismatch |
The key was already bound to a different payload; investigate before creating a new intent. |
422 business rule |
Correct facts/request or use declared review path; no blind retry. |
429 |
Wait at least Retry-After, then retry with same key. |
Transient 5xx / 503 |
Exponential backoff with jitter and same key. |
Bound retries by time and surface unresolved unknown outcomes to operations. Exactly-once business effect comes from idempotent intent, provider correlation, authoritative posting, and reconciliation—not HTTP retries alone.
Implemented Servicing Workspace mutations use the same optional 1–50 character Idempotency-Key convention: supplying a key opts into replay protection, omitting it gives that attempt a fresh server-owned key. Signed channel callbacks and dispatch-bound voice callbacks deduplicate on their closed connector identity instead.
Rate limits, and backing off with the same key
Limits are tenant-, client-, and endpoint-aware. A 429 includes Retry-After and a request ID. Spread batch work, honor concurrency limits, page through lists instead of refetching them, and prefer webhooks over aggressive polling.