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

# Error handling

> Detect, decide and act on the six ways a payment fails, without moving value twice.

A rejected request created nothing, and [Errors](/errors) lists its code and whether it is safe to retry. A failing payment exists and has state in `status` and `operationalState`, with values on [Status codes](/status-codes); it fails in one of six ways.

<Warning>
  **Never create a replacement payment for one that is stuck or uncertain.** Reuse the original payment and its idempotency key. A second payment for the same obligation is how value moves twice.
</Warning>

## A payer never pays: let it expire

**Detect.** `status` becomes `expired` and `payment.expired` fires. The deadline is `expiresAt`, set from `expiresInSeconds` at creation: 60 to 86400 seconds, defaulting to 600.

**Decide.** Expiry is terminal. The payment does not reopen, and funds arriving afterwards do not revive it.

**Act.** Stop polling. If the payer wants another attempt, create a new payment with a new idempotency key. To close the window early, cancel before any funds are detected.

```bash theme={null}
curl -X POST https://staging-api-v2.stableyard.fi/v2/payments/payment_123/cancel \
  -u "$APP_ID:$APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "reasonCode": "abandoned", "reason": "Payer left the flow" }'
```

`reasonCode` is `customer_request`, `duplicate`, `abandoned` or `other`. Cancel works only before provider detection or verified funds, and replaying it on an already-cancelled payment returns the same resource. Set `expiresInSeconds` to how long you will genuinely hold the obligation: a long window produces more late receipts, and a late receipt is worked by a person rather than your code.

## A quote expires: refresh the option or start again

**Detect.** `payment_quote_expired`, HTTP 409.

**Decide.** The quote lapsed. Read the Payment to establish its current state and reconcile any funds already sent.

**Act.** Refresh a checkout option on the existing Payment only when it is eligible and no payment evidence has been observed. An external payout quote cannot be refreshed on the same Payment. Start a new attempt with a new key only after the original is terminal and any funds sent have been reconciled. Never replace a processing or uncertain payout. See [Conversion](/payments/conversion#handle-an-expired-quote-by-where-it-lived).

## A method becomes unavailable: re-read and present what is left

**Detect.** `payment_method_not_supported` (422) for this destination, chain or asset. `payment_amount_below_minimum` (422) when a route's floor moves above the amount. `payment_not_open` (409) when the payment no longer accepts options or funds.

**Decide.** Availability is decided per app, environment, market and destination, at runtime. A method you read an hour ago is not a method you hold now.

**Act.** Re-read `GET /v2/partners/config` and the payment itself, then present what is left; if nothing is left for that destination, say so. None of these three codes is retryable: the identical request gets the identical refusal. [View supported regions and currencies →](/supported-regions-and-currencies)

## A provider is down: retry with the same key

**Detect.** `payment_provider_unavailable` and `provider_unavailable` return 503. `payment_provider_error` and `provider_failure` return 502. A timeout with no response is the same class of problem.

**Decide.** An error or a timeout tells you nothing about whether the operation happened.

**Act.** Retry with backoff, reusing the **same** `Idempotency-Key` and a byte-identical body. If the first attempt landed, you get that result back instead of a second movement. Then read the payment before acting. If the retry returns `request_in_progress` (409), the first request is still running: wait and read rather than retrying harder. See [Idempotency](/idempotency).

<Warning>
  One case deliberately does not retry. Where an external rail may already have created a pending transaction, the result is marked `provider_state_uncertain` and no automatic resubmission happens, because a retry could pay the beneficiary twice. Read the payment and escalate it instead.
</Warning>

## A transfer is reverted: reverse your credit

**Detect.** `payment.settlement_returned` for a settlement that came back after delivery. `deposit.reversed` for a settled deposit whose accounting record was invalidated. On the payment, `operationalReasonCode` carries `settlement_failed`, `settlement_requires_intervention` or `settlement_unconfirmed`.

**Decide.** A return after delivery is an accounting correction, not a change to the financial result. A `succeeded` payment does not become `failed` because settlement came back, and `deposit.reversed` does not imply an on-chain clawback.

**Act.** Reverse the credit on your side. Nothing resends automatically and there is no automatic replacement, so do not tell the customer value is on its way back.

## A payment needs intervention: hold and escalate

**Detect.** `operationalState` is `requires_intervention` and `payment.requires_intervention` fires. `operationalReasonCode` names the cause.

```json theme={null}
{
  "status": "accepted",
  "stage": "settlement_broadcast",
  "operationalState": "requires_intervention",
  "operationalReasonCode": "settlement_requires_intervention"
}
```

**Decide.** Automation stopped on purpose. The financial `status` is preserved exactly, so reading `status` alone says the payment is healthy when it is not. `retrying` is the opposite case: automation is still working and needs nothing from you.

**Act.** Put the payment in front of a named person with its id, your `externalReference` and the customer attached. Hold whatever credit you were about to give. Do not create a replacement payment, and do not retry the original request. Nothing builds that queue for you; the events that populate it are on [Platform tools](/payments/platform-tools).

## When you do not know what happened

<Steps>
  <Step title="Create nothing new">
    No replacement payment, no second send, no fresh idempotency key. The first attempt may have succeeded.
  </Step>

  <Step title="Replay the identical request with the identical key">
    An exact replay returns the original result rather than creating a second payment.
  </Step>

  <Step title="Read the payment">
    By id, or by your `externalReference` through `GET /v2/payments`. See [Reconciliation](/payments/reconciliation).
  </Step>

  <Step title="If it needs intervention, stop">
    Escalate with the payment id. Retrying does not move it forward.
  </Step>
</Steps>

<Card title="Next: Platform tools" icon="toolbox" href="/payments/platform-tools">
  The events to subscribe to, and how to rehearse in sandbox.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.