Skip to content

Resilience and retries

Networks fail, processes restart, and processors are occasionally slow. An integration that moves money has to assume every call can fail halfway and still never charge twice or lose a payment.

Set a client timeout

Aptean Pay does not cut long requests short, so your HTTP client must. A card payment normally completes in a few seconds. Pick a timeout that tolerates a slow processor, such as 60 seconds, rather than one that gives up during a normal call.

A timeout does not mean the call failed. It means you do not know. Treat it as the ambiguous case below.

What is safe to retry

Call Retry?
Any query Yes. Queries change nothing.
createPayment, createRefund Yes, with the same idempotency key. Never with a new one.
upsertPaymentRequest Yes. A create retried with the same referenceNumber is refused as a duplicate rather than creating a second request.
upsertCreditMemo, upsertCustomer with an id Yes. It updates the same record.
upsertCreditMemo, upsertCustomer without an id No. Each call creates a new record. Look for the first one before retrying. A duplicate credit memo gives the payer twice the credit.
capturePayment, cancelPayment Read the payment first. If it already shows the result you wanted, you are done.
Anything refused with errors[] No. It fails the same way until you change the request.
A processor decline (payment.status: FAILED) Not with the same card. See Errors.

Retry only on network errors, timeouts and HTTP 5xx responses. Use exponential backoff with jitter, for example 1, 2, 4 then 8 seconds, and cap the number of attempts.

Recovering from an ambiguous failure

When a createPayment call times out or the connection drops:

  1. Retry with the same idempotency key.
  2. If the retry returns a payment, use it.
  3. If the retry returns IDEMPOTENCY_KEY_ALREADY_USED, the first attempt succeeded. Do not charge again. Find the payment by the orderNumber or invoiceNumber you set and record it.
  4. If the API is unreachable for longer, keep the order in an "unknown" state and reconcile it later. Never mark it unpaid on a guess.

The same steps apply to createRefund.

Store what you need, as soon as you have it

  • Persist payment.id the moment a call returns it. It is your only handle for capture, cancel, refund and status checks.
  • Persist the idempotency key before the first attempt.
  • Set invoiceNumber, orderNumber and customerPONumber on every payment. They are how you find a payment when you have lost its id.

Following payments without webhooks

Aptean Pay does not call your system when anything changes. To learn that an ACH payment cleared, a payment request was paid, or a payment settled, you read it.

  • Poll only what is open. Track PENDING payments and unpaid payment requests, and query those.
  • Match the cadence to the payment type. Card payments resolve in seconds. ACH takes days, so check it daily.
  • Stop at a terminal state. COMPLETED, FAILED and CANCELED do not change on their own. A completed payment can still be refunded or voided, but only by an action someone takes.
  • Page and filter on the server. Use date ranges and status filters instead of re-reading your whole history.

More detail is in Payment status.

Keep the payer out of retry loops

  • Disable the pay button while a payment is in flight, so a double-click cannot send two payments. Each one would carry its own idempotency key.
  • After a decline, ask for a different payment method. Repeatedly retrying a declined card counts against the merchant with the card networks.