# Error handling and retry strategy

Payments Direct returns a [standardized error response](/products/payments-direct-2/v2026.03/api-docs/error-handling/payments-direct-api-errors) for every failed API request. Building a reliable integration means classifying errors correctly, retrying safely, and escalating promptly when retrying won't help.

This topic covers:

- How to classify errors using the `code` and `type` fields
- Which errors are safe to retry, and how to retry them
- Recommended exponential backoff parameters
- Handling authentication errors
- Handling payment failures (FAILED, DECLINED, RETURNED states)
- What to log and monitor


## Classify before you act

Every error response includes a `code` field (for example, `USR_067`) and a `type` field (for example, `USER_ERROR`). Always use the `code` field as your primary signal, not the HTTP status code alone. Two errors can share an HTTP status but require completely different handling.

### Error type reference

| Type | Prefix | What it means | General action |
|  --- | --- | --- | --- |
| `USER_ERROR` or `NOT_FOUND` | `USR_` | The request itself is the problem: missing fields, invalid values, or a resource that doesn't exist. | Fix the request and resubmit. Do not retry the same request. |
| `AUTH_ERROR` | `AUTH_` | The request was rejected due to an authentication or authorization problem. | See [Handling authentication errors](#handling-authentication-errors) below. |
| `SYSTEM_ERROR` | `SYS_` | An internal error occurred in Ripple's infrastructure. | Retry with exponential backoff. Escalate to Ripple technical support if the condition persists. |
| `CONFIGURATION_ERROR` | `CFG_` | An account or service configuration issue prevents the request from completing. | Do not retry. Contact Ripple technical support. |


## Transient vs. permanent errors

Before retrying, confirm that the error is transient (a condition that may resolve itself) rather than permanent.

| Error type | HTTP status | Retryable? | Recommended action |
|  --- | --- | --- | --- |
| `USER_ERROR` | 400, 404, 415 | No | Fix the request and resubmit. |
| `USER_ERROR` | 402 | Not until resolved | Resolve the underlying condition (insufficient balance, credit limit, or past-due invoice) before retrying. |
| `NOT_FOUND` | 404 | No | Verify the resource ID and resubmit. |
| `AUTH_ERROR` | 401 | Yes (after token refresh) | Regenerate your access token and retry. |
| `AUTH_ERROR` | 403 | No | Verify your token has the required scopes. Contact Ripple technical support if the issue persists. |
| `SYSTEM_ERROR` | 500 | Yes (with backoff) | Retry with exponential backoff. Contact Ripple technical support if the issue persists. |
| `CONFIGURATION_ERROR` | 500 | No | Contact Ripple technical support. |


## Retry strategy

### Exponential backoff with jitter

When a request fails with a retryable error (`SYSTEM_ERROR` / 500, or `AUTH_ERROR` / 401 after a token refresh), use exponential backoff with jitter rather than retrying immediately or at a fixed interval. Immediate or synchronized retries amplify load on an already stressed system and can trigger rate limiting.

Recommended approach:

1. On the first failure, wait a short base interval.
2. On each subsequent failure, double the wait time.
3. Add random jitter (±10–20% of the interval) to desynchronize retries across clients.
4. Cap the maximum wait at a reasonable ceiling.
5. After a configurable number of attempts, stop retrying and alert your on-call team.


**Example backoff schedule:**

| Retry attempt | Base wait | With jitter (±20%) |
|  --- | --- | --- |
| 1 | 2s | 1.6s – 2.4s |
| 2 | 4s | 3.2s – 4.8s |
| 3 | 8s | 6.4s – 9.6s |
| 4 | 16s | 12.8s – 19.2s |
| 5 | 32s | 25.6s – 38.4s |
| 6+ | 60s (cap) | 48s – 72s |


### Use a maximum retry count

Set a hard limit on the number of retries (for example, 5 attempts). After that limit is reached without a successful response, stop retrying, record the failure, and alert your team. Continuing to retry indefinitely can mask a persistent issue and delay investigation.

## Idempotency and safe retries

A timeout or connection error on a create payment request does not mean the payment was not created. Ripple may have received and processed the request even though you did not receive the response.

### The `quoteId` is the idempotency key

Payment creation is deduplicated on `quoteId`, and one quote can produce at most one payment. If a payment already exists for the `quoteId` in a `POST /v3/payments` request, Ripple returns that payment instead of creating a new one.

When you resend a request for a quote that already has a payment:

- The response is `201 Created`, the same as for a new payment. The status code does not tell you whether this request created the payment or returned an existing one.
- The payment is returned as it was stored. Ripple does not compare the rest of the request body against it, so changing a field on a resend has no effect. Resend the original request unchanged.
- The payment is returned even if the quote has expired since the first request.


`internalId` is not an idempotency key for payments. Ripple does not check payment `internalId` values for uniqueness, so two requests with the same `internalId` and different `quoteId` values create two payments. This differs from identities, where `internalId` must be unique across your ACTIVE identities. See [Payment identities](/products/payments-direct-2/v2026.03/introduction/concepts/payment-identities#idempotency-and-deduplication-with-internalid).

### Retrying after a timeout

Retry with the same quoteId, never a new quote
If a create payment request times out or fails with a connection error, resend it with the same `quoteId`. If the first request created the payment, the resend returns it. If it did not, the resend creates it.

Do not get a new quote and submit it in place of the first. A new `quoteId` is a new payment, and Ripple cannot tell that it duplicates the first one, even if both requests carry the same `internalId`.

Space resends out using the backoff described in [Retry strategy](#retry-strategy).

If a resend is rejected with HTTP `400` and `USR_058` (quote expired), no payment was stored for that quote when the resend was processed. Before you get a new quote, check that the original request did not create a payment, as described below.

### Checking whether a payment was created

To reconcile payments against your own records, search with `POST /v3/payments/filter` and your `internalId`. This is still a valid check, with two limits:

- Search results are updated asynchronously. A payment created moments ago may not appear yet, so an empty result immediately after a timeout does not prove that the payment was not created.
- The `internalId` filter is not an exact-match lookup. Confirm that the `internalId` on each returned payment matches yours exactly.


Because of these limits, resending with the same `quoteId` is the reliable way to recover from a timeout. Use the search to reconcile, not to decide whether to resend.

## Handling authentication errors

### 401 Unauthorized (AUTH_001, AUTH_003)

A 401 error typically means your access token is expired or invalid. Access tokens have a 1-hour TTL.

**Recommended handling:**

1. Generate a new access token using your `client_id` and `client_secret`.
2. Retry the original request with the new token.
3. Do not cache tokens beyond their `expires_in` value.


For guidance on token generation and caching, see [Authentication](/products/payments-direct-2/v2026.03/api-docs/get-started/authentication).

### 403 Forbidden (AUTH_002)

A 403 error means your token is valid but lacks the required scopes for the requested operation.

**Recommended handling:**

- Do not retry with the same token. A new token with the same credentials will have the same scopes.
- Review the required scopes for the operation in the API reference.
- Contact Ripple technical support if you believe your credentials should have the required permissions.


### 403 Unauthorized (AUTH_051)

AUTH_051 occurs when a request references a payment created by a different organization. This is a permanent error. Do not retry.

## Handling payment failures

Payment failures are distinct from API errors. A payment failure occurs after a payment is created successfully (HTTP 201) but later transitions to a terminal state of `FAILED`, `DECLINED`, or `RETURNED`. These are not errors in the API response. They are `paymentState` values you observe when polling or receiving webhooks.

How to detect a payment failure
Payment failure details, including the error code and reason, are available on the payment object at `GET /v3/payments/{paymentId}` after the payment reaches a terminal state. Webhook payloads include the new `paymentState` but not the error code. Always fetch the full payment record to get failure details.

### Terminal states

| State | Retryable? | Action |
|  --- | --- | --- |
| `FAILED` | Possibly | Fetch the payment record to get the error code. Check the [API payment failures reference](/products/payments-direct-2/v2026.03/api-docs/error-handling/api-payment-failures) and [API error codes reference](/products/payments-direct-2/v2026.03/api-docs/error-handling/api-errors) for the specific code. Some `FAILED` states are caused by transient issues and can be retried with a new payment; others indicate a permanent condition. |
| `DECLINED` | Not until resolved | Fetch the payment record to get the error code. `DECLINED` usually indicates a business rule violation or account condition (insufficient balance, credit limit, etc.) that must be resolved before resubmitting. |
| `RETURNED` | Consult Ripple | The payment was returned by a downstream institution after completing. This is not a retry scenario. Contact Ripple technical support to investigate the return reason. |


For a full list of payment failure codes and their descriptions, see [API payment failures](/products/payments-direct-2/v2026.03/api-docs/error-handling/api-payment-failures).

For background on payment states and the payment lifecycle, see [Payment states](/products/payments-direct-2/v2026.03/introduction/concepts/payment-lifecycle).

## What to log and monitor

Log the following fields from every error response:

- `code` — the specific error code; use this for alerting rules and dashboards
- `type` — the error category; use this to route to the correct handler
- `status` — the HTTP status code
- `timestamp` — the time the error occurred in the upstream system
- The API endpoint and HTTP method that returned the error
- Your request ID or correlation ID (if your integration generates one)


### Alerting recommendations

| Condition | Alert priority |
|  --- | --- |
| Any `CONFIGURATION_ERROR` (`CFG_`) | High — these require Ripple involvement and will not self-resolve |
| Repeated `SYSTEM_ERROR` (`SYS_`) from the same endpoint | Medium — investigate after retry budget is exhausted |
| `AUTH_ERROR` 403 (`AUTH_002`, `AUTH_051`) | Medium — may indicate a configuration change or credential scope issue |
| Sustained `USER_ERROR` 402 (`USR_062`–`USR_067`) | Medium — indicates an account condition (balance, limits, invoice) requiring attention |


## Next steps

- For the complete list of error codes, descriptions, and HTTP statuses, see [API error codes](/products/payments-direct-2/v2026.03/api-docs/error-handling/api-errors).
- For payment-specific failure codes, see [API payment failures](/products/payments-direct-2/v2026.03/api-docs/error-handling/api-payment-failures).
- For guidance on tracking payment state changes without polling, see [Notification webhooks](/products/payments-direct-2/v2026.03/api-docs/payment-monitoring/notification-webhooks).
- To poll for payment updates instead of using webhooks, see [Polling](/products/payments-direct-2/v2026.03/api-docs/payment-monitoring/polling).
- For access token generation and caching details, see [Authentication](/products/payments-direct-2/v2026.03/api-docs/get-started/authentication).
- For payment state definitions and lifecycle details, see [Payment states](/products/payments-direct-2/v2026.03/introduction/concepts/payment-lifecycle).