Skip to content

Idempotency

If a call that moves money times out, you cannot tell whether it ran. The idempotency key lets you retry it safely: Aptean Pay refuses to run the same operation twice.

Where it is required

Two mutations require an idempotency-key header and will not run without one:

Mutation Why
createPayment A blind retry could charge the payer twice.
createRefund A blind retry could refund twice.
{
  "x-aptean-apim": "<your api key>",
  "x-aptean-tenant": "<your tenant id>",
  "x-aptean-tenant-secret": "<your tenant secret>",
  "idempotency-key": "<a unique value you generate>"
}

Other mutations ignore the header. The upserts (upsertPaymentRequest, upsertCreditMemo, upsertCustomer) work differently: pass the record's id and they update it, so re-sending is harmless. Without an id they create a new record each time. The exception is upsertPaymentRequest, which refuses a second request with the same referenceNumber. See Resilience and retries.

The rules

  • You generate it. Any value unique to that one operation works. A UUID is the usual choice.
  • It is scoped to the merchant and the operation type. The same key does not collide across tenants, or between a payment and a refund.
  • A key that succeeded cannot be used again. A second call with it is refused with idempotency key has already been used, a unique key must be provided and reason code IDEMPOTENCY_KEY_ALREADY_USED.
  • A key whose attempt failed can be used again. If the payment was declined or cancelled, or the call errored, the key is released and a retry with the same key runs normally.
  • A repeat does not return the original result. It returns the error above. To see what the first attempt did, read the payment.
  • Keys are kept for at least 24 hours. Do not design anything that depends on a key expiring.

Generate the key with the operation, not the request

Create the key when you create the order, invoice payment or refund in your own system, and store it on that record. Then every retry, including one after your process restarts, sends the same key.

order 1042 created    -> idempotency_key = 6f1c...  (stored on the order)
createPayment         -> timeout
createPayment (retry) -> same key 6f1c...

If you generate a fresh key for each HTTP request, idempotency cannot protect you: the retry looks like a new payment.

Handling the outcomes of a retry

Retry result Meaning Do
code: "SUCCESS" The first attempt never ran, or failed. This one went through. Record the payment.
code: "ERROR", payment FAILED Declined. The key is released. Ask for another payment method.
IDEMPOTENCY_KEY_ALREADY_USED The first attempt succeeded (or is still running). Do not charge again. Find the payment and record it.
idempotency key is required The header is missing. Send it.

To find the payment after IDEMPOTENCY_KEY_ALREADY_USED, query recent payments for that merchant and match on the orderNumber or invoiceNumber you set. This is one reason to always set them. See Payment status.

Warning

Never respond to IDEMPOTENCY_KEY_ALREADY_USED by generating a new key and trying again. That is exactly the double charge the key exists to prevent.