Idempotency
Networks fail mid-request. A connection drops after the server has already created an application but before the response reaches you. An idempotency key lets you safely retry any write — the server returns the original result instead of opening a second loan, taking a second payment, or sending a second disbursement.
Every state-changing call on the Credicorp lending API — opening an application, accepting an offer, scheduling a collection, initiating a PISP payment — accepts an Idempotency-Key request header. Because these operations move real money and create real liabilities against a UK company, sending one is not optional housekeeping: it is the difference between a retry that is invisible and a retry that double-charges a borrower. This guide covers how keys behave, how to choose them, how long they live, and the one error you will see if you get it wrong.
Every Credicorp SDK sends an idempotency key automatically on each create call, generating a fresh UUID per logical operation and re-using it across the client's built-in retry loop. You only need to set one by hand when you want a deterministic key derived from your own records — which we recommend below.
How it works
Send an Idempotency-Key header on any mutating request. The first request the platform sees with a given key is processed normally; its HTTP status and response body are then recorded against that key. Any later request carrying the same key and the same payload skips processing entirely and replays the stored response — identical status code, identical body, plus an Idempotency-Replayed: true header so you can tell a replay from a fresh result in your logs.
# First attempt — processed, result stored against the key curl -s https://api.credicorp.co.uk/partner/v1/applications \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: ord_5512-apply" \ -H "Content-Type: application/json" \ -d '{ "business": {"company_number":"16093826"}, "amount_pence":2500000, "term_months":12 }' # Connection dropped? Re-run byte-for-byte — you get the SAME application back, # not a second one. Note the Idempotency-Replayed: true response header.
Replay is keyed on three things together: your project, the request method-and-path, and the key value. A key used on POST /applications does not collide with the same string used on POST /payments — they are different operations and tracked separately.
Requests still in flight
If a retry arrives while the first request is still being processed — common when a client times out aggressively — the platform does not run the operation twice. The second request short-circuits with 409 Conflict and code idempotency_key_in_use. Back off briefly and retry; once the first call settles, the same key replays its stored result.
Choosing keys
A good key is unique to one logical operation and stable across every retry of it. There are two sound strategies.
| Strategy | Example | When to use |
|---|---|---|
| Random UUID | 9b1f7c2e-3a44-4e90-bf21-a01d5e | Generate once before the first attempt; cache it for the retry loop. Simplest correct option. |
| Deterministic | ord_5512-apply | Derive from your own primary key. Survives a process crash — on restart you recompute the identical key. |
Deterministic keys are the stronger pattern for lending. If your worker dies after sending the request but before persisting the response, a random key is lost — on restart you would mint a new one and risk a duplicate. A key built from a value you already store (an order id, a quote id, a job id) is reproducible: the restarted worker computes the same string and the platform replays the original result.
Namespace deterministic keys by operation, e.g. ord_5512-apply, ord_5512-accept, ord_5512-collect-01. One order legitimately drives several distinct writes; a per-operation suffix keeps each safely retryable without colliding with the others.
Key requirements
- A string of up to 255 characters — UUIDs, ULIDs and your own ids all fit comfortably.
- Unique per logical operation. Re-use it across retries of that operation only, never across two different operations.
- Scoped to your project and retained for 24 hours after the first use. After that the key is forgotten and a fresh request with it would be processed as new.
Scope & retention
Keys live for 24 hours from first use, per project. Two consequences worth designing around:
- Project isolation
- Your sandbox and live projects keep separate key spaces. A key burned in the sandbox is free to use live, and vice versa.
- The 24h window
- A retry is only safe to assume idempotent within 24 hours. If you queue a job that might not run for longer than that, lean on a deterministic key plus your own “already submitted?” check before sending.
The reuse conflict
The platform stores a fingerprint of the request body alongside each key. If you send the same key with a different payload, that is almost always a bug — two distinct operations accidentally sharing a key — so the API refuses it rather than guess which one you meant:
HTTP/1.1 422 Unprocessable Entity { "error": { "type": "invalid_request", "code": "idempotency_key_reused", "message": "This Idempotency-Key was already used with a different request payload.", "param": "Idempotency-Key", "original_request_id": "req_2Vtq8K" } }
To resolve it, either send the original body with that key (you will get the first result back), or pick a new key for the genuinely new operation. The original_request_id points at the first request that claimed the key, which makes the collision easy to trace in your logs and in the dashboard.
Which calls take a key?
| Method | Idempotency key |
|---|---|
| POST create / action | Strongly recommended. Required for applications, payments and payments/schedules — the calls that move money or create liabilities. |
| PUTPATCH update | Optional. These are naturally idempotent (they set state), but a key still protects against duplicate side-effects. |
| GET read | Not needed — reads change nothing and are safe to repeat freely. |
| DELETE cancel | Optional. Cancelling twice is harmless; a key makes the response consistent across retries. |
A correct retry loop
The pattern: generate the key once, then reuse it for every attempt. Retry only on network errors and 5xx / 429 responses; a 4xx other than 409 is a permanent client error and should not be retried.
import { Credicorp } from '@credicorp/sdk'; const cc = new Credicorp({ token: process.env.CREDICORP_TOKEN }); // Deterministic key from YOUR order id — survives a crash & restart. const key = `${order.id}-apply`; const app = await cc.applications.create({ business: { company_number: order.company_number }, amount_pence: order.amount_pence, term_months: order.term_months, reference: order.id, }, { idempotencyKey: key }); // re-used automatically across the SDK's retries
use Credicorp\Client; $cc = new Client(getenv('CREDICORP_TOKEN')); $key = $order->id . '-apply'; // deterministic, reproducible on restart $app = $cc->applications->create([ 'business' => ['company_number' => $order->company_number], 'amount_pence' => $order->amount_pence, 'term_months' => $order->term_months, 'reference' => $order->id, ], idempotencyKey: $key);
# Mint the key ONCE, then loop the same value with backoff. KEY="ord_5512-apply" for attempt in 1 2 3; do curl -sf https://api.credicorp.co.uk/partner/v1/applications \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $KEY" \ -d '{ "business": {"company_number":"16093826"}, "amount_pence":2500000, "term_months":12, "reference":"ord_5512" }' \ && break sleep $((attempt * 2)) # exponential-ish backoff; same KEY each time done
Generate the key before the first attempt and keep it for the entire retry loop. Minting a fresh key on each retry defeats the entire mechanism — every “retry” would then look like a brand-new operation and could open a second loan or take a second payment.
Best practice, in short
- Always send a key on money-moving POSTs — applications, payments, schedules. Treat it as mandatory in your client.
- Prefer deterministic keys derived from your own ids; they survive crashes that random keys do not.
- One key, one operation. Suffix by action when a single record drives several writes.
- Reuse across retries only. The first request defines the result; retries replay it.
- Retry network errors,
5xxand429with the same key and backoff. Do not retry other4xx. - Design for the 24-hour window. Beyond it, fall back to your own duplicate check.
Idempotency on requests pairs with idempotency on the receiving side: webhook deliveries can also arrive more than once, so handlers must dedupe on event.id. See Webhooks and the Integrate lending guide for the end-to-end picture.
