Skip to main content

Idempotency

Every state-changing call carries an spTransactionId that you generate. It is how the platform guarantees that a retry cannot become a second charge, and it is the single most important field in this API, because a double charge cannot be apologised away.

How it works

Three steps, in this order, on every write:

  1. Claim the transaction id.
  2. Run the operation.
  3. Store the outcome against that id.

The claim happens before anything is spent, which is what makes the guarantee hold even if your connection drops mid-call or our process dies between steps.

What happens when you send the same id twice

With an identical request body, you get the stored outcome back, verbatim. The operation does not run again. Whatever happened the first time (success or a specific failure) is what you receive.

The requestId will differ, and that is correct

A replay returns the original outcome under the current call's requestId. requestId identifies this HTTP call; the outcome belongs to the transaction. So two entries in your log with different requestIds and the same spTransactionId are one transaction, not two.

With any changed field, you get DUPLICATE_TRANSACTION_ID (409). The platform fingerprints the validated request body, so a reused id with a different amount, number or service is refused rather than silently re-run.

This is the behaviour that catches people out. If your first attempt failed because a parameter was wrong, correcting the parameter and retrying under the same id will be refused. A corrected request is a new attempt: give it a new id. The id identifies an attempt, not an intention.

Generating one

  • Unique per attempt. A UUID or ULID is ideal.
  • Generated by you, stored by you, alongside your own record of the call: you will need it to reconcile.
  • Never derived from user input, and never reused across services or endpoints.

When a retry is safe

SituationSame id, or new?
You got a response and want the same answer againSame id: you will get the stored outcome
Your connection dropped and you do not know the outcomeSame id: this is exactly what it is for
You got retryable: true and are backing offSame id: nothing terminal was stored
You are correcting a rejected parameterNew id: it is a new attempt
You are charging the same subscriber again, deliberatelyNew id: it is a new transaction

When you do not know whether a call succeeded, retrying with the same id is always the right move. That is the situation the mechanism exists for: it either replays the answer you missed, or runs the operation that never happened. It cannot do both.

Transient failures leave nothing behind

If a call fails in a way marked retryable: true, no terminal outcome is stored: the claim is released. Retrying with the same id genuinely re-executes rather than replaying a failure.

This matters most with the anti-fraud check: an unavailable verdict consumes nothing, so your retry is a real second attempt rather than a stored refusal handed back to you.

What it does not protect

Idempotency is per transaction, not per subscriber. It stops one attempt becoming two charges. It does not stop you deliberately charging the same subscriber twice with two different ids: that is a correct use of the API, and the limits on Error codes are what bound it.