# Payment errors (/en/integrate/errors/payment-errors)

When a customer's payment fails, the error sits on the Payment Intent's lastPaymentError field. The shape, the adviceCode, and how to split copy between your customer and your team.

This page covers the **asynchronous** failure surface. When a customer's payment fails (card declined, processor timeout, expired card, etc.), the error doesn't come back as an HTTP response to your server. It lives on the Payment Intent's `lastPaymentError` field for the rest of its life.

For synchronous errors returned by your own API calls, see [API errors](/integrate/errors/api-errors).

## Where the error lives [#where-the-error-lives]

A failed payment leaves three distinct breadcrumbs:

* **`paymentIntent.lastPaymentError`**: the full structured detail. Read this for everything you need to show in your ops UI, decide retry strategy, or build customer-facing copy.
* **`charge.failureCode` and `charge.failureMessage`**: the same headline error, attached to the specific Charge that failed. Useful when you key fulfillment off the Charge.
* **The `charge.failed` webhook**: fires the moment a charge attempt fails. Subscribe to it if you need to react to failures in real time.

The most complete data is on `lastPaymentError`. If your handler receives `charge.failed`, fetch the Payment Intent to get the structured fields described below.

## The `lastPaymentError` shape [#the-lastpaymenterror-shape]

```json
{
  "type": "card_error",
  "code": "card_declined",
  "declineCode": "insufficient_funds",
  "networkDeclineCode": "51",
  "message": "Your card has insufficient funds.",
  "merchantMessage": "Declined: insufficient funds. Customer should use a different payment method.",
  "adviceCode": "try_again_later",
  "docUrl": "https://docs.xpay.app/integrate/errors/decline-codes#insufficient_funds",
  "param": null,
  "chargeId": "ch_test_AbC123",
  "paymentMethodType": "card",
  "paymentMethod": { "card": { "brand": "mastercard", "last4": "0008" } },
  "processorCode": "...",
  "processorMessage": "..."
}
```

| Field                               | Meaning                                                                                                                                                                            |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                              | Broad category: `card_error`, `payment_method_error` (BNPL, wallet, kiosk, bank transfer), `processor_error`, `api_error`.                                                         |
| `code`                              | The specific error reason. From the [Payment error codes](/integrate/errors/payment-error-codes) list.                                                                             |
| `declineCode`                       | For card declines, the issuer's reason. From the [Decline codes](/integrate/errors/decline-codes) list. `null` for non-decline failures.                                           |
| `networkDeclineCode`                | The 2-4 digit code returned by the card network (e.g. `51` for insufficient funds in ISO 8583). The meaning depends on the card brand. `null` when the network didn't provide one. |
| `message`                           | Short message safe to surface to the customer.                                                                                                                                     |
| `merchantMessage`                   | Detailed message for your support team, includes context the customer shouldn't see.                                                                                               |
| `adviceCode`                        | What to do next: `confirm_card_data`, `try_again_later`, or `do_not_try_again`. The single most actionable field.                                                                  |
| `docUrl`                            | Deep link to the matching row in the codes reference.                                                                                                                              |
| `param`                             | The request body field that failed, if applicable. Usually `null` on this surface.                                                                                                 |
| `chargeId`                          | The `ch_*` ID of the Charge that failed.                                                                                                                                           |
| `paymentMethodType`                 | The kind of payment method (`card`, `valu`, `fawry`, etc.).                                                                                                                        |
| `paymentMethod`                     | Snapshot of the payment method at the time of failure (brand, last4 for cards).                                                                                                    |
| `processorCode`, `processorMessage` | Raw codes and messages from the processor, untranslated. For deep debugging only.                                                                                                  |

`code` is always set. `declineCode` and `networkDeclineCode` are set on card declines; the rest fill in based on what XPay can extract from the processor response.

## The three code spaces [#the-three-code-spaces]

`lastPaymentError` carries up to three different codes for the same failure. Each is the source of truth for a different question:

| Field                | Question it answers                        | Where to find descriptions                                   |
| -------------------- | ------------------------------------------ | ------------------------------------------------------------ |
| `code`               | What kind of failure was this?             | [Payment error codes](/integrate/errors/payment-error-codes) |
| `declineCode`        | Why did the issuer decline? (cards only)   | [Decline codes](/integrate/errors/decline-codes)             |
| `networkDeclineCode` | What raw code did the card network return? | Brand-specific; pass through to support if needed            |

For most handlers, branch on `adviceCode` first, surface `merchantMessage` in your ops UI, and use `code` or `declineCode` to pick customer-facing copy.

## The handler pattern [#the-handler-pattern]

The action you take is decided by `adviceCode`, not by `code`. The code list grows; the three advice values don't.

| `adviceCode`        | What it means                                                                            | What to do                                                                                                                        |
| ------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `confirm_card_data` | The card data was probably wrong (CVV mismatch, incorrect number, postal code mismatch). | Tell the customer to re-enter their card. Don't suggest a different card; the same card with corrected details is likely to work. |
| `try_again_later`   | Transient: insufficient funds, processor timeout, network glitch.                        | Tell the customer to try again, or surface a "try a different payment method" option. Safe to retry once.                         |
| `do_not_try_again`  | Hard reject: stolen card, lost card, fraud, expired card.                                | Don't retry the same card. Show the customer a "use a different payment method" prompt. Notify your team if the rate spikes.      |

```typescript
function handlePaymentFailure(error: LastPaymentError) {
  // Always log first. The merchantMessage is what your support team needs.
  log.error("payment failed", {
    chargeId: error.chargeId,
    type: error.type,
    code: error.code,
    declineCode: error.declineCode,
    networkDeclineCode: error.networkDeclineCode,
    merchantMessage: error.merchantMessage,
    docUrl: error.docUrl,
  });

  // Decide UX from adviceCode.
  switch (error.adviceCode) {
    case "confirm_card_data":
      return showRetryWithCard(error.message);

    case "try_again_later":
      return showRetryWithFallback(error.message);

    case "do_not_try_again":
      return showUseDifferentMethod(error.message);

    default:
      // Missing adviceCode is rare. Treat as do_not_try_again to be safe.
      return showUseDifferentMethod(error.message);
  }
}
```

## Customer copy vs merchant copy [#customer-copy-vs-merchant-copy]

XPay returns two messages on every failure. They're not interchangeable.

| Field             | Audience                                                                                                        | Example                                                                         |
| ----------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `message`         | Your customer. Already filtered for what's safe to display.                                                     | "Your card has insufficient funds."                                             |
| `merchantMessage` | Your team (support, ops dashboards). Includes context that's useful internally but inappropriate for customers. | "Declined: insufficient funds. Customer should use a different payment method." |

The dashboard's transaction tooltip uses `merchantMessage`. Build your own ops UI the same way. For the customer-facing surface (your checkout page, your transactional emails), use `message` or your own copy keyed off `code` / `declineCode`.

Don't show `merchantMessage` to customers. It can leak internal context. Don't show `processorCode` or `processorMessage` to customers either; those are raw processor passthrough, untranslated.

## Reading `lastPaymentError` in webhooks [#reading-lastpaymenterror-in-webhooks]

The `charge.failed` webhook carries a Charge in `event.data.object`. The Charge has the headline failure (`failureCode`, `failureMessage`) but not the full structured detail. To read the full `lastPaymentError`, fetch the parent Payment Intent:

```typescript
async function onChargeFailed(event: { data: { object: Charge } }) {
  const charge = event.data.object;

  // Quick action: the Charge alone has the headline.
  log.warn("charge failed", {
    chargeId: charge.id,
    failureCode: charge.failureCode,
    failureMessage: charge.failureMessage,
  });

  // Full detail: fetch the PI.
  const pi = await getPaymentIntent(charge.paymentIntentId);
  if (pi.lastPaymentError) {
    handlePaymentFailure(pi.lastPaymentError);
  }
}
```

`lastPaymentError` is set on the PI for the lifetime of the resource. It records the most recent failed attempt; subsequent retries on the same PI overwrite it.

## What not to do [#what-not-to-do]

* **Don't branch on `code` alone.** The code list grows over time. Branch on `adviceCode` for what to do, then use `code` for narrow copy decisions.
* **Don't show `merchantMessage` to your customers.** It can include details that are inappropriate for the consumer surface.
* **Don't ignore `adviceCode: "do_not_try_again"`.** Retrying after a stolen-card or fraud decline doesn't help and can flag your account to the issuer.
* **Don't blindly retry on `try_again_later`.** Retry once, then move the customer to a different method or end the flow. Looping on a `do_not_honor` decline produces the same result every time.
* **Don't use `networkDeclineCode` as your primary branch.** It's a raw passthrough that depends on the card brand. Use `declineCode` (XPay's normalized form) instead.

## Where to next [#where-to-next]

<Cards>
  <Card icon="<ListTree />" title="Payment error codes" href="/integrate/errors/payment-error-codes">
    Every value of `lastPaymentError.code`, with the customer-safe and merchant-facing copy XPay
    returns.
  </Card>

  <Card icon="<Ban />" title="Decline codes" href="/integrate/errors/decline-codes">
    The issuer's reason for the decline (`lastPaymentError.declineCode`). The most specific code
    XPay carries.
  </Card>

  <Card icon="<TriangleAlert />" title="API errors" href="/integrate/errors/api-errors">
    The other surface: synchronous errors returned in your API call's HTTP response.
  </Card>

  <Card icon="<Activity />" title="Events panel" href="/integrate/workbench/events-panel">
    See every `charge.failed` event in the dashboard. Resend, drill into the attempt, jump to the
    Payment Intent.
  </Card>

  <Card icon="<Terminal />" title="Logs panel" href="/integrate/workbench/logs-panel">
    The original API call that produced the failed Charge. Useful for cross-referencing.
  </Card>

  <Card icon="<Layers />" title="Errors introduction" href="/integrate/errors/introduction">
    Back to the top. The two-surfaces map.
  </Card>
</Cards>