## Overview

When a payment reaches `FAILED` status, Reservepay attaches a `failure_code` that tells you why. These failure codes are canonical across payment methods: `insufficient_funds` means the same thing no matter how the customer paid. Underneath, each payment method's bank or provider returns its own response code or message, and Reservepay maps that raw response onto one of these shared codes.

This guide lists the failure codes Reservepay produces for card payments, split between the card authorization itself and the 3D Secure authentication step that precedes it.

## Where to read the failure code

Failure codes live on the payment object as two fields:

| Field | Type | Description |
| :--- | :--- | :--- |
| `failure_code` | `string` | A machine-readable identifier for the failure (e.g., `insufficient_funds`). |
| `failure_message` | `string` | A short description of why the payment failed. |

Both fields are `null` unless the payment failed. Retrieve them with `merchants/find-payment`. The payment list and search endpoints such as `merchants/list-payments` and `merchants/filter-payments` return the same payment object, so the same two fields are available there.

The `payment_completed` webhook fires when a payment transitions to either `SUCCESSFUL` or `FAILED`, but its payload carries only `status`, not the failure code. When you receive `payment_completed` with a `status` of `FAILED`, call `merchants/find-payment` with the `payment_id` from the event to read `failure_code`.

Failure codes are separate from the API error triples returned when a request itself is rejected. See [Error Handling](/guides/error-handling) for those. A failed payment is a successful API call describing a declined payment.

## How to handle failure codes

Branch your logic on the machine-readable `failure_code`, not on the human-readable `failure_message`. Messages exist for surfacing to users, and codes are stable across bank wording changes. A few codes are worth handling distinctly from a plain decline:

* **`issuer_not_available`** means the card issuer couldn't be reached, so the payment was never authorized. This is often transient, so it's reasonable to let the customer retry immediately.
* **`generic_decline`** is Reservepay's catch-all for a raw response that doesn't map to anything more specific yet. Treat it the same as any other decline, and don't build logic that depends on it disappearing over time.
* Everything else, including `insufficient_funds`, `invalid_cvv`, `expired_card`, and `limit_exceeded`, is a terminal decline for that attempt. Show the customer the message and let them choose a different payment method or card.
