> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crossmint.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Handle Errors

> Show buyers why a payment was declined, and react to order failures

When a card is declined during an Apple Pay or Google Pay purchase, the wallet sheet closes with only the wallet's generic failure state — for example, Apple Pay shows "Order failed" with no explanation. Without a visible decline reason, buyers tend to retry the same card several times, which lowers their chance of a successful purchase.

Crossmint always delivers a structured decline reason to your page. This guide shows the two ways to surface it, and how to tell a decline apart from a user cancellation.

## Prerequisites

* **Embedded Checkout**: An [Embedded Checkout](https://docs.crossmint.com/payments/embedded/overview.md) integration with Apple Pay or Google Pay enabled. See [Payment Methods](https://docs.crossmint.com/payments/concepts/payment-methods.md).
* **React SDK**: The code samples use `@crossmint/client-sdk-react-ui` with `CrossmintCheckoutProvider`. See [Hooks & Events](https://docs.crossmint.com/payments/guides/hooks-and-events.md).

## Option 1: Enable the Built-In Error Banner

The checkout component includes an error banner that displays the decline reason. On the Apple Pay and Google Pay screens this banner is hidden by default. Enable it with one appearance rule — **`appearance.rules.GlobalMessage.display: "visible"`**:

```tsx theme={null}
<CrossmintEmbeddedCheckout
    // ...other props
    appearance={{
        rules: {
            GlobalMessage: { display: "visible" },
        },
    }}
/>
```

With this rule set, the checkout shows the decline reason above the payment button after the wallet sheet closes.

> [!NOTE]
>
> The built-in banner dismisses automatically after 8 seconds. If your page constrains the checkout iframe's height,
> the banner can render outside the visible area. For full control over placement and persistence, use Option 2.

## Option 2: Render Your Own Error Message

The checkout reports every order change to your page. After a decline, the updated order contains `payment.failureReason`. Read it with the `useCrossmintCheckout` hook and render the message anywhere on your page:

```tsx theme={null}
import { useCrossmintCheckout } from "@crossmint/client-sdk-react-ui";

function CheckoutErrorBanner() {
    const { order } = useCrossmintCheckout();
    const failureReason = order?.payment?.failureReason;

    if (failureReason == null) {
        return null;
    }
    return (
        <div role="alert" className="checkout-error">
            {failureReason.message ?? "Your payment was declined. Try a different payment method."}
        </div>
    );
}
```

Place the component where the buyer is guaranteed to see it — directly adjacent to the checkout, or in a sticky status region. Do not place it where it can fall outside the visible viewport.

Keep listening after the sheet closes. With Google Pay, the sheet closes as soon as the buyer authorizes, and the decline can arrive afterwards. Some declines also arrive asynchronously while the order is finalized. React to `failureReason` whenever it appears, and clear your error UI when the buyer starts a new attempt.

## Tell a Cancellation Apart from a Decline

The sheet closing is ambiguous on its own: it happens both when the buyer cancels and when the card is declined. Use the order state to distinguish the two:

| Signal | Meaning | What to show |
| - | - | - |
| Sheet closes and `order.payment.failureReason` stays `undefined` | The buyer canceled | Nothing, or a neutral "Payment not completed" message |
| An order update arrives with `order.payment.failureReason` set | The card was declined | The decline explanation (`failureReason.message`) |

A cancellation never produces a `failureReason`, and a decline always does. Gate your error UI strictly on `failureReason` — never on the sheet closing.

## The `failureReason` Object

| Field | Type | Description |
| - | - | - |
| `code` | `string` | Normalized decline code for logic, analytics, and support tooling |
| `category` | `string` (optional) | The broad reason the payment failed, such as `authorization_decline` or `risk_block` |
| `retryPolicy` | `string` (optional) | The recommended next step for the buyer, such as `retry_with_different_payment_method` |
| `message` | `string` (optional) | User-safe explanation. **The only field safe to render to buyers** |

The complete list of categories, codes, and retry policies lives in the [Error & Status Codes](https://docs.crossmint.com/payments/reference/error-and-status-codes.md) reference. Use `retryPolicy` to decide which affordance to show — for example, prompt for a different card on `retry_with_different_payment_method`, and hide the retry button on `do_not_retry`.

A card decline returns the order to the `awaiting-payment` state regardless of the decline code, so the order can still accept a payment. `awaiting-payment` therefore means the order is open, not that retrying is a good idea — `retryPolicy` is the signal for that. With `do_not_retry` (for example `psp_security_block`, where fraud or risk screening blocked the attempt), do not offer a retry even though the order remains `awaiting-payment`. Never retry a declined payment automatically on the buyer's behalf.

> [!NOTE]
>
> `awaiting-payment` together with `retryPolicy: "do_not_retry"` is a valid, expected combination. Crossmint does not
> enforce `do_not_retry` server-side; it is a hint for your UI.

## Delivery Failures

A payment can succeed while delivery fails — for example if minting fails. The order still reaches `phase: "completed"`, with the affected line item's `delivery.status` set to `failed`. Failed items are refunded automatically; show the buyer a refund notice and a way to purchase again. See [How Checkout Works](https://docs.crossmint.com/payments/concepts/how-checkout-works.md#completed-phase).

## Common Gotchas

> [accordion] The error message renders but the buyer never sees it
>
> The checkout iframe's height is managed dynamically by the SDK. If your page wraps the iframe in a fixed-height
> container with internal scrolling, content near the bottom — including the built-in error banner — can land
> below the fold. Let the SDK manage the iframe height, or render your own error message outside the iframe
> (Option 2).

> [accordion] The decline arrives after the sheet is already closed
>
> This is expected, particularly with Google Pay, where authorization completes before the charge is attempted.
> Keep observing order updates after the sheet closes instead of checking once.

## Next Steps

> [card] [Error & Status Codes](https://docs.crossmint.com/payments/reference/error-and-status-codes.md)

> [card] [Appearance](https://docs.crossmint.com/payments/guides/appearance.md)

> [card] [Track Orders](https://docs.crossmint.com/payments/guides/track-orders.md)
