
5 minutes
Handling retries, timeouts and edge cases without breaking checkout
How your integration responds to inevitable payment errors determines whether a failed transaction becomes a recovered sale or an abandoned cart.
Simon Farrow
Solution Architect
Simon Farrow is an IT architect specialist with 10 years’ experience in payment integrations, specializing in secure, scalable enterprise payment infrastructure.
Key points
- Not all payment errors are equal. 4xx errors mean the request must be fixed before retrying; 5xx errors usually point to temporary server-side issues where a retry may work. Treating them the same creates avoidable failures and load.
- Checkout resilience depends on three things: accurate error classification, retry logic with appropriate back-off and user messaging that separates recoverable issues from hard stops. Most integrations only get one right.
- Worldpay, now Global Payments, error responses use a standard JSON structure with machine-readable error names, human-readable messages and, for validation errors, JSONPath references to the failed field. This supports automated handling without relying on free-text messages, which may change.
This article is part of a series focused on helping developers design, build and scale payment integrations with practical guidance and examples.
A checkout that fails silently is worse than one that fails loudly. Silent failures lose the sale and the customer; loud failures, handled well, can recover both.
Most payment integrations treat error handling as a late-stage concern – something to revisit after the happy path is working. The cost of that sequencing shows up in production: retries that cause duplicate charges, timeout handling that abandons recoverable transactions and user messages that say "something went wrong" without giving the customer a way forward.
Learn how our error response structure works, how to classify errors correctly and how to build retry and timeout logic that holds up under load.
How our error responses are structured
Our Access APIs return errors in a standardized JSON format alongside an HTTP status code. Understand this structure before you write error handling logic – retrofitting it later costs more time.
The generic error format
Every error response contains at minimum:
- errorName – a machine-readable string identifying the type of error. Same error name means same cause and same resolution, across all Access APIs. This is the field your automated handling logic should branch on.
- message – a human-readable description with contextual detail. Useful for logging and debugging. Not for machine consumption – the format is not guaranteed to be stable across API versions.
Validation errors: the nested format
When a request fails schema validation, the response includes a validationErrors array containing one entry per failed field. Each entry carries:
- errorName – the specific validation failure (e.g. fieldIsMissing, fieldMustBeNumber, stringIsTooLong)
- message – human-readable description of the failure
- jsonPath – JSONPath reference to the specific field in the request body (e.g. $.amount, $.paymentInstrument.type)
The jsonPath field is the practical differentiator here. Rather than parsing the message string to locate the problem – which will break when messages change – your validation error handling can use jsonPath to map failures directly back to specific fields in your request construction logic.
HTTP status codes: the first branch in your error logic
Before handling the error body, branch on the HTTP status code range. Branching correctly between 4xx and 5xx matters more than any other decision in your error handling logic.
4xx: Fix the request before retrying
4xx errors indicate a problem with the request itself. The server received and understood the call; the call was wrong. Common examples:
- 400 – bad request. Covers missing headers (headerIsMissing), invalid body (bodyIsNotJson), schema failures (bodyDoesNotMatchSchema), and configuration mismatches (entityIsNotConfigured, currencyIsNotSupported)
- 401 – authentication failure. Credentials are missing, invalid or expired.
- 406 / 415 – header value errors. The Accept or Content-Type header does not match expected values (headerHasInvalidValue)
Retrying a 4xx without changing the request will produce the same error. The fix is in your code, not in waiting and trying again.
The one 4xx exception: a 400 with entityIsNotConfigured
The one 4xx exception worth noting: entityIsNotConfigured and paymentInstrumentIsNotSupported indicate configuration issues on the merchant account rather than errors in the request itself. These need to be resolved via your Relationship Manager, not by changing your request payload.
5xx: retry with care
5xx errors indicate server-side problems. They range from configuration issues affecting a specific merchant entity, to transient network failures that will resolve themselves. The defining characteristic is that you typically cannot fix them by changing the request.
- 500 (internalErrorOccurred) – an internal error, potentially from a downstream service interaction. May be transient.
- 503 (serviceUnavailable) – the service cannot fulfil the request, even though it is functioning internally. Typically transient.
5xx errors are candidates for retry. Timeouts (the most common form) fall here. The question is not whether to retry, but how.
Retry logic: the rules that matter
Exponential back-off with jitter
Retrying immediately after a 5xx adds load to a service that is already under pressure. Retry with exponential back-off: Wait before the first retry, double the wait on each subsequent attempt, and add a random jitter component to prevent synchronized retry storms from multiple clients hitting the same endpoint at the same time.
A practical starting point: first retry after 1 second, second after 2 seconds, third after 4 seconds. Cap total retries at 3–5 attempts depending on the criticality of the operation and your acceptable latency budget.
Idempotency: the guardrail against duplicate charges
Retrying payment calls risks creating duplicate transactions. The customer's card gets charged twice; you have two orders for one intent. This is the error that generates chargebacks and support escalations.
That protection comes from idempotency keys. Include a unique idempotency key in each payment request. If the same key is submitted again, the API returns the result of the original request rather than processing a new transaction. Your retry logic can safely re-submit without risk of duplication.
Generate idempotency keys per transaction intent, not per API call. If a network timeout means you don't know whether the first call succeeded, re-submitting with the same key resolves the uncertainty safely.
What not to retry
Not every 5xx warrants a retry in front of the user. If your first two or three attempts fail and you're in a checkout flow, the right response is to fail gracefully and give the customer a way to try again – not to keep the session open while you exhaust your retry budget. Background and async payment operations can afford a longer retry window; synchronous checkout cannot.
Never retry a 4xx. Never retry without an idempotency key on a call that creates a transaction. Never swallow a 5xx and report success.
Timeout handling
Timeouts are the most common 5xx scenario and the most mishandled. A timeout means the server did not respond within your configured window – it does not mean the transaction failed. The request may have been received and processed; you simply didn't get the response.
"A timeout is an unknown, not a failure. Treat it as one."
This is the scenario where idempotency keys matter most. When a call times out:
- Log the timeout with the idempotency key and transaction reference
- Wait for the back-off interval before retrying with the same idempotency key
- On retry success, verify the transaction state through our reporting or query APIs before confirming the order
- On continued failure, surface a recoverable error to the customer – not a generic error message
A timeout is an unknown, not a failure. Treat it as one.
Building user-facing messages that don't lose the sale
The technical handling of errors is only half the problem. The other half is what the customer sees.
Most checkout errors fall into three categories from the user's perspective, and each requires a different response:
1. Recoverable: Try again or try differently
Transient failures and soft declines are recoverable. The customer's intent is genuine; the transaction can succeed with another attempt or a different payment method. Your messaging should:
- Acknowledge that something went wrong without blaming the customer
- Offer a clear next action: Try again, use a different card, check the card details
- Preserve the cart and session – do not force the customer to start over
2. Actionable: The customer needs to do something specific
Some errors point to a specific fixable problem: expired card, incorrect CVC, billing address mismatch. These are not generic failures – they have a specific resolution. Surface that information directly rather than hiding it behind a generic error message.
Use the errorName from the response – not the message field – to drive your UI logic. Error names are stable; messages are not.
3. Hard stops: Do not retry, do not let the customer retry
Fraud flags, sanctioned entities and certain compliance failures should not be retried and should not prompt the customer to try a different card. These require a different flow entirely – typically escalating to your support process rather than asking the customer to keep attempting.
Hard stops are identifiable by error name. Build explicit handling for them rather than letting them fall through to your generic error catch.
The error names worth building explicit handling for
Our error response reference documents the full set of error names. Based on frequency and impact in checkout flows, these warrant explicit handling rather than generic fallback:
- bodyDoesNotMatchSchema – parse the validationErrors array and use jsonPath to identify the specific field. Log with full detail for debugging.
- entityIsNotConfigured / currencyIsNotSupported – configuration issues, not request errors. Alert your engineering team; do not surface to the customer as a retry scenario.
- paymentInstrumentIsNotSupported – the payment method is not enabled for this merchant entity. Do not prompt the customer to retry with the same method.
- underlyingPaymentInstrumentHasExpired – the stored token or saved card has expired. Prompt the customer to update their payment details rather than retry.
- internalErrorOccurred / serviceUnavailable – 5xx candidates for retry with back-off and idempotency key. If retries are exhausted, surface a recoverable error and offer the customer an alternative.
Testing your error handling before go-live
Error handling that has never been tested in anger will fail in production. Before go-live:
- Use our sandbox to simulate the error scenarios your integration needs to handle. Do not only test the happy path.
- Test timeout behavior: Configure a short timeout in your sandbox environment and verify your retry logic fires correctly with the same idempotency key.
- Test duplicate submission: Submit the same idempotency key twice and confirm only one transaction is created.
- Test your user-facing messages: Walk through each error scenario as a customer would and verify the messaging is actionable, not generic.
- Test your logging: Confirm that errors are captured with enough detail to diagnose – error name, HTTP status, idempotency key, transaction reference – without logging sensitive card data.
Further reading
Related insights


