◀ Knowledge hub

05, Payments & monetization

Idempotency keys for payments and callbacks

codeAmani Labs Engineering
Cinematic still for Idempotency keys for payments and callbacks

The network will deliver your important request twice

Retries are not an edge case, they are how distributed systems stay reliable. A client whose connection drops mid request does not know whether the server got it, so it sends again. A payment callback that is not acknowledged in time is redelivered. If "charge the card" or "credit the wallet" runs twice, you have charged a customer twice. Idempotency keys make a repeated request safe.

The idea in one sentence

An idempotent operation produces the same result whether it runs once or many times. You achieve it by attaching a unique key to the request and recording, atomically, that the key has been handled, so the second arrival is recognized and skipped.

Implementing it with the database as the referee

The database is the right place to enforce "exactly once", because a unique constraint is atomic and survives a crash. Insert the key first; if the insert fails on the constraint, you have seen this request before.

create table processed_requests (
  idempotency_key text primary key,
  result jsonb,
  created_at timestamptz default now()
);
try {
  await sql`insert into processed_requests (idempotency_key) values (${key})`;
} catch (e) {
  if (isUniqueViolation(e)) return cachedResult(key); // already done
  throw e;
}
const result = await doTheWork();
await sql`update processed_requests set result = ${result} where idempotency_key = ${key}`;
return result;

Where the key comes from

For client requests, the client generates the key and sends it in a header, reusing the same key on every retry of the same logical action. For provider webhooks, the provider's event id or transaction id is the key; you do not invent it, you adopt theirs. Either way, the key identifies the intent, not the attempt.

This is the same discipline everywhere

Idempotency is not a payments trick, it is a system wide habit. M-Pesa callbacks rely on it, Stripe webhooks rely on it, the queue consumer that processes a job at least once relies on it. Anywhere a message can arrive more than once, which is everywhere that matters, the handler must be safe to run again. Build it in from the start and a whole category of "we charged them twice" incidents never happens.

Qualified conversation

Have a build to de-risk? Let's talk.

Tell us what you are building. We respond within two business days.