> ## 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.

# Payments

> The Payment object: intent, amount, destination, frozen terms, both state axes, refunds and errors.

A `Payment` is one bounded movement of value into or out of an account, with a fixed amount, an expiry and a final result. It keeps one `payment_` id from creation to the last webhook, and there is no separate transfer, payout or conversion resource.

## When to use it

| `intent` | Moves value | You name | How it executes |
| - | - | - | - |
| `receive` | Into an account, or straight to an external wallet | `recipient` | The payer selects a payment option and funds the payment's escrow |
| `send` | Out of an account | `sender` and `destination` | The sender's payment source funds it, and you confirm the returned action |

`intent` never changes after creation. For open-ended top-ups by a returning customer, issue a [deposit address](/concepts/deposit-addresses) instead: a payment is one amount, one expiry and one reference.

## The object

| Field | What it holds |
| - | - |
| `id` | `payment_` prefix. The only payment id the API accepts |
| `apiVersion` | The contract version that shaped this resource |
| `intent` | `receive` or `send` |
| `amountMode` | `collect_exact` or `deliver_exact`. See [Amount and amount mode](#amount-and-amount-mode) |
| `paymentAmount` | The obligation: `amount`, `amountAtomic`, `assetType`, `assetCode`, `decimals`, plus `chainId` and `tokenAddress` for a token |
| `sourceAmount` | Send only: the total sender debit, commercial fees included. Returned to callers permitted to see sender fees |
| `participants` | `{ role, accountId }` entries, `role` being `sender` or `recipient`. Only accounts your tenant owns are returned, so the array can be empty |
| `recipientDisplay` | `displayName` and `logoUrl` captured from the recipient's display profile at creation, or null |
| `destination` | The resolved counterparty. See [Destination types](#destination-types) |
| `settlement` | Receive only: where value lands. Null for payment types without a receive settlement destination |
| `fees` | The commercial fee terms, returned only to the partner that owns them |
| `funding` | Fiat sends only: collection terms, including any available quote. Null otherwise |
| `status`, `operationalState`, `operationalReasonCode`, `operationalUpdatedAt`, `stage`, `offrampStatus`, `statusVersion` | State. See [States](#states) |
| `refundSummary` | Aggregate progress of the attached refunds |
| `incidentRecoverySummary` | Recovery of duplicate, late, overpaid and underpaid receipts. Never counted in `refundSummary` |
| `externalReference`, `description`, `metadata` | Your values, echoed back. `externalReference` is the list filter |
| `expiresAt`, `acceptedAt`, `succeededAt`, `cancelledAt`, `createdAt`, `updatedAt` | Timestamps |
| `failure` | `{ code, message }`, or null |

Create, get, confirm and cancel return the payment inside an envelope: `{ payment, checkout, nextAction }`.

* **`checkout`** carries `paymentUrl`, `clientSecret`, `expiresAt` and `returnUrl`. It appears only on the response that creates a receive payment, and `clientSecret` is never returned again.
* **`nextAction`** is the step owed now. `type` is `transaction`, `managed_authorization` or `payment_method` with an `id` and `expiresAt`, or one of `start_kyc_session`, `verify_account_email`, `complete_compliance` (with `url` and `expiresAt`), `wait_for_partner_kyc`, `contact_support` or `none`. It is null when nothing is owed, and always once the payment is terminal.

`GET /v2/payments` returns the bare resources, without `checkout` or `nextAction`.

## Amount and amount mode

`paymentAmount` takes exactly one of `amount` (a decimal string) or `amountAtomic` (an integer string, with `decimals`), plus `assetCode`. `assetType` defaults to `crypto`. The body is validated strictly: an unknown key is rejected.

| Route | `amountMode` | `paymentAmount` is |
| - | - | - |
| Receive | `collect_exact`, the only value and the default | Crypto: the amount collected |
| Send to `upa`, `payment_handle` or `crypto_wallet` | `deliver_exact`, the only value and the default | Crypto: what the recipient receives. Fees are added on top in `sourceAmount` |
| Send to `external_bank` or `external_qr` | `deliver_exact` | Fiat: what the beneficiary receives |
| Send to `bank_account` | `deliver_exact` with a fiat amount, or `collect_exact` with a crypto amount, as the route requires | See [Sending payments](/payments/sending-payments#create-the-payment) |

`expiresInSeconds` sets the funding window, from 60 to 86400, with a default of 600. `mandateId` is accepted only on sends to the three crypto destinations.

## Recipient on a receive

| `recipient` | Settles to |
| - | - |
| `{ "accountId": … }` or `{ "externalUserId": … }` | That account's current settlement profile |
| `{ "wallet": { "chainId", "address", "assetCode" } }`, optional `tokenAddress` | That wallet directly, with no account involved |

`settlement.settlementDestinationId` overrides the destination for this one payment and is accepted only when the recipient is an account. A recipient account needs an active settlement destination, or the create is refused. See [Settlement destinations](/settlement/destinations#override-the-destination-for-one-payment).

## Destination types

A send names a `destination`. Stableyard resolves it and freezes the resolved shape at creation.

| `destination.type` | Request fields | Resolved `destination` on the payment |
| - | - | - |
| `upa` | `account`: `{ "accountId" }` or `{ "externalUserId" }` | `accountId` |
| `payment_handle` | `paymentHandle`: bare `alice` or qualified `alice@partner` | `paymentHandle` as `name@namespace`, and the `accountId` it resolved to |
| `crypto_wallet` | `chainId`, `address`, `assetCode`, optional `tokenAddress` | `chainId`, `address`, `assetCode`, `tokenAddress` |
| `external_bank` | `country`, `bankCode`, `accountNumber`. `beneficiaryName` is required for `PH` and optional for `VN` | `country`, `currency`, `beneficiary`: `accountHolderName`, `accountNumberLast4`, `bankCode`, `bankName`, `country` |
| `bank_account` | `bankAccountId` of a linked, verified bank under the sender | `bankAccountId`, `country`, `currency`, `bankName`, `accountNumberLast4`, `rail` |
| `external_qr` | `country`, `qrPayload` | `country`, `currency`, `dynamic`, `merchant` as `{ name, city }` or null |

* **Account numbers are write-only.** The resolved destination carries the last four digits only.
* **`upa` and `payment_handle` take the recipient's asset.** The recipient's settlement profile decides the chain and asset.
* **A qualified handle can belong to another partner.** On a send, `name@namespace` resolves across namespaces; see [Wallets and handles](/concepts/wallets-and-handles#resolve-a-handle).
* **Fiat destinations are enabled per market and per app.** Read `GET /v2/partners/config` before offering one. A personal QR code is refused with `bad_request` and `details.reasonCode: "personal_qr_not_supported"`.

The flows for each are on [Sending payments](/payments/sending-payments#address-any-destination-type), [Stablecoin transfers](/payments/stablecoin-transfers) and [Off-ramps](/payments/off-ramps).

## Frozen at creation

| Snapshot | Fields | What it fixes |
| - | - | - |
| `fees` | `version`, `pricingStatus`, `platformFeeBps`, `partnerFeeBps`, `platformFeeAmountAtomic`, `partnerFeeAmountAtomic`, `merchantNetAmountAtomic` | The commercial charge. With `pricingStatus: "quote_pending"` the rates are frozen and the three amounts are null; with `quoted` the amounts are exact and immutable |
| `settlement` | `type` (`settlement_destination` or `crypto_wallet`), `settlementDestinationId`, `destinationType`, `destinationAddress`, `chainId`, `assetCode`, `tokenAddress`, `decimals` | Where a receive payment lands. Checkout cannot change it |
| `funding` | `required`, `providerPrincipal`, `providerFee`, `platformFee`, `partnerFee`, `totalFees`, `exchangeRate`, `quoteExpiresAt`, `depositAddress` | A fiat send's collection terms. `totalFees` is already inside `required` |
| `recipientDisplay` | `displayName`, `logoUrl` | What payers see as the recipient |

A later change to the account's settlement profile, its display profile or your fee rates never rewrites an existing payment. Fee arithmetic is on [Assessing fees](/payments/assessing-fees), and quote deadlines on [Conversion](/payments/conversion).

## Payment options on a receive

A payer funds a receive payment through one option, created by the checkout with the payment's `checkout.clientSecret` at `POST /v2/public/payments/{paymentId}/options`.

| Option field | Values |
| - | - |
| `id` | `pay_option_` prefix |
| `paymentMethodType` | `crypto`, `fiat_onramp`, `card`, `account_balance` |
| `status` | `creating`, `pending`, `processing`, `succeeded`, `expired`, `failed`, `cancelled` |
| `selectionStatus` | `selected` or `superseded`. One option is selected at a time |
| `confirmationMode` | `provider_webhook`, or `transaction_hash_submission` when the payer must submit a hash |
| `refreshable` | True only when the option expired or failed and no financial evidence was observed |

Selecting an option never changes the obligation, the recipient, the settlement snapshot or the fees. The full option and the checkout calls are on [Depositing funds](/payments/depositing-funds#build-your-own-checkout).

## States

| Field | Values | Read it for |
| - | - | - |
| `status` | `requires_payment_method`, `requires_action`, `processing`, `accepted`, `succeeded`, `failed`, `cancelled`, `expired` | The financial result. `succeeded`, `failed`, `cancelled` and `expired` are terminal |
| `operationalState` | `normal`, `retrying`, `requires_intervention` | Whether automation is retrying or a person must act. `operationalReasonCode` names the cause |
| `offrampStatus` | `not_started`, `processing`, `failed`, `refunding`, `refunded`, `succeeded`, or null | The external fiat payout. Null on every other payment type |
| `stage` | `escrow_provisioning`, `awaiting_payment`, `destination_verifying`, `provider_processing`, `quote_pending`, `transaction_broadcast`, `receipt_verifying`, `payment_detected`, `settlement_pending`, `settlement_broadcast`, `settlement_confirming`, `settlement_returned`, `payout_pending`, `payout_processing`, `payout_confirming`, `refund_pending`, `refund_broadcast`, or null | Diagnostics only. It can move backwards |
| `statusVersion` | Integer from 1 | Increases by one whenever `status`, `stage`, `operationalState` or `operationalReasonCode` changes. Discard a read with a lower value |

<Warning>
  `accepted` means verified funds with settlement not finished; `succeeded` means the destination was credited. A status never regresses after `accepted`: a later settlement, fee payout or refund problem moves `operationalState` instead, so a payment can be `succeeded` and `requires_intervention` at once.
</Warning>

Meanings and every `operationalReasonCode` are on [Status codes](/status-codes). What to store is on [Reconciliation](/payments/reconciliation#store-these-fields).

## Create and read

```bash theme={null}
curl -X POST https://staging-api-v2.stableyard.fi/v2/payments \
  -u "$APP_ID:$APP_SECRET" \
  -H "Idempotency-Key: deposit:order_1042" \
  -H "Content-Type: application/json" \
  -d '{
    "intent": "receive",
    "recipient": { "accountId": "acct_123" },
    "paymentAmount": { "amount": "25.00", "assetType": "crypto", "assetCode": "USDC", "chainId": 42161 },
    "externalReference": "order_1042",
    "expiresInSeconds": 600
  }'
```

```json theme={null}
{
  "payment": {
    "id": "payment_123",
    "intent": "receive",
    "amountMode": "collect_exact",
    "status": "requires_payment_method",
    "operationalState": "normal",
    "statusVersion": 1,
    "paymentAmount": {
      "amount": "25.00",
      "amountAtomic": "25000000",
      "assetType": "crypto",
      "assetCode": "USDC",
      "decimals": 6,
      "chainId": 42161,
      "tokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
    },
    "participants": [{ "role": "recipient", "accountId": "acct_123" }],
    "settlement": {
      "type": "settlement_destination",
      "settlementDestinationId": "destination_123",
      "destinationType": "connected_wallet",
      "destinationAddress": "0x1111111111111111111111111111111111111111",
      "chainId": 42161,
      "assetCode": "USDC",
      "tokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
      "decimals": 6
    },
    "funding": null,
    "refundSummary": { "status": "none", "count": 0, "requestedAmountAtomic": "0", "confirmedAmountAtomic": "0" },
    "externalReference": "order_1042",
    "expiresAt": "2026-10-01T12:10:00.000Z"
  },
  "checkout": {
    "paymentUrl": "https://pay.stableyard.fi/pay/7Yf3KMpQ2xWa",
    "clientSecret": "pay_client_secret_opaque_value_123",
    "expiresAt": "2026-10-01T12:10:00.000Z",
    "returnUrl": null
  },
  "nextAction": { "id": "payment_123", "type": "payment_method", "expiresAt": "2026-10-01T12:10:00.000Z" }
}
```

| Call | Endpoint | Notes | Guide |
| - | - | - | - |
| Create | `POST /v2/payments` | `Idempotency-Key` required, up to 256 characters | [Depositing funds](/payments/depositing-funds), [Sending payments](/payments/sending-payments) |
| Preview a send | `POST /v2/payments/preview` | Read-only, no key. Crypto destinations only | [Sending payments](/payments/sending-payments#preview-before-you-commit) |
| Get | `GET /v2/payments/{paymentId}` | The current state and `nextAction` | [Reconciliation](/payments/reconciliation) |
| List | `GET /v2/payments` | Filters: `accountId`, `externalUserId`, `externalReference`, `intent`, `status`. `limit` 1 to 100, default 20; page with `cursor` until `nextCursor` is null | [Reconciliation](/payments/reconciliation#sweep-on-your-own-schedule) |
| Checkout link | `GET /v2/payments/{paymentId}/checkout` | `paymentUrl`, `expiresAt`, `returnUrl`, `paymentStatus`, `canAcceptPayment`. Never the client secret | [Depositing funds](/payments/depositing-funds#collect-a-known-amount-with-a-hosted-payment-page) |
| Confirm a send | `POST /v2/payments/{paymentId}/confirm` | `actionId` and a `proof` of the type `nextAction` names | [Sending payments](/payments/sending-payments#complete-the-returned-action) |
| Cancel an unpaid receive | `POST /v2/payments/{paymentId}/cancel` | `reasonCode`: `customer_request`, `duplicate`, `abandoned` or `other`; optional `reason`. Replaying returns the cancelled payment | [Error handling](/payments/error-handling) |
| Refund | `POST /v2/payments/{paymentId}/refunds` | `Idempotency-Key` required. See [Refunds](#refunds) | [Reconciliation](/payments/reconciliation#tell-a-recovery-from-a-refund) |
| List or get refunds | `GET /v2/payments/{paymentId}/refunds`, `GET /v2/refunds/{refundId}` | Newest first, cursor-paginated | [Reconciliation](/payments/reconciliation#tell-a-recovery-from-a-refund) |

Only the receiving participant can cancel, and only before funds are detected. A send cannot be cancelled.

## Refunds

A `Refund` returns verified funds from a receive payment and is tracked as its own record. Stableyard derives the destination from the verified payer evidence, on the escrow's chain and token; you cannot choose or override it.

```bash theme={null}
curl -X POST https://staging-api-v2.stableyard.fi/v2/payments/payment_123/refunds \
  -u "$APP_ID:$APP_SECRET" \
  -H "Idempotency-Key: refund:order_1042:1" \
  -H "Content-Type: application/json" \
  -d '{ "amount": "5.00", "reasonCode": "customer_request", "reason": "Customer returned one item" }'
```

The body takes exactly one of `amount` or `amountAtomic`, a `reasonCode` of `customer_request` or `operational`, and a `reason` of 1 to 500 characters. A `202` returns the refund:

```json theme={null}
{
  "refund": {
    "id": "refund_123",
    "paymentId": "payment_123",
    "status": "created",
    "amount": "5.00",
    "amountAtomic": "5000000",
    "assetCode": "USDC",
    "chainId": 42161,
    "transactionHash": null,
    "reasonCode": "customer_request",
    "reason": "Customer returned one item",
    "createdAt": "2026-10-01T12:20:00.000Z",
    "broadcastAt": null,
    "confirmedAt": null,
    "failure": null
  }
}
```

| Rule | Detail |
| - | - |
| Eligible payments | A collected receive payment funded through a direct crypto option from one verified payer address. Provider-funded and multi-payer payments need a provider or manual recovery workflow |
| Amount | Paid from the payment's escrow, so it is limited to accepted value that has not already settled out or been refunded |
| Who can refund | The partner that owns the receiving account. Anyone else gets `not_found` |
| `status` | `created`, `pending_approval`, `signing`, `retry_wait`, `broadcast`, `confirmed`, `failed`, `requires_intervention`, `cancelled`. `confirmed` is the only state that means the payer was paid back |
| `reasonCode` on reads | Also `duplicate_payment`, `late_payment`, `overpayment`, `underpayment` and `offramp_provider_terminal_failure`, for recoveries Stableyard started |

On the payment, `refundSummary` aggregates the refunds as `status` (`none`, `pending`, `partially_refunded`, `refunded`, `failed`, `requires_intervention`), `count`, `requestedAmountAtomic` and `confirmedAmountAtomic`.

## Errors

Branch on `error.code`, and on `details.code` or `details.reasonCode` when the top-level code is `conflict`. The full list is on [Errors](/errors).

| Code | HTTP | When |
| - | - | - |
| `bad_request` | 400 | Missing `Idempotency-Key`, an unknown field, an `amountMode` or `assetType` that does not match the route, or a recipient account with no settlement destination |
| `forbidden` | 403 | A cancel from anyone but the receiving participant, or a fiat rail your app is not enabled for |
| `idempotency_conflict` | 409 | The same `Idempotency-Key` with a different body |
| `request_in_progress` | 409 | An identical request is still being processed. Read the payment, then decide |
| `conflict` | 409 | `details.reasonCode: "payment_confirmation_not_supported"` on a confirm for a payment with no outgoing execution |
| `payment_already_detected` | 409 | A cancel after funds were detected |
| `payment_quote_expired` | 409 | A fiat payout quote cannot be refreshed: read the original Payment and reconcile any funds sent before a new attempt after terminal status. Refresh a checkout option only when eligible and no payment evidence was observed |
| `settlement_profile_required` | 422 | The recipient account's settlement profile or destination is not active |
| `payment_method_not_supported` | 422 | The route is not available for this destination, chain or asset |
| `refund_route_not_supported` | 422 | The payment was not funded by a direct crypto option from one payer address |
| `refund_amount_exceeds_option` | 422 | More than the unsettled, unrefunded accepted amount |
| `insufficient_escrow_balance` | 409 | The escrow cannot cover the refund |
| `payment_provider_unavailable` | 503 | The provider is down, or a fiat quote leaves too little time to fund (`details.minimumQuoteWindowSeconds`). Retry with the same key |

## Webhooks

| Group | Events |
| - | - |
| Lifecycle | `payment.created`, `payment.requires_action`, `payment.processing`, `payment.accepted`, `payment.succeeded`, `payment.failed`, `payment.cancelled`, `payment.expired` |
| Operations | `payment.requires_intervention`, `payment.duplicate_received`, `payment.late_received`, `payment.settlement_returned` |
| Refunds | `payment.refund_pending`, `payment.partially_refunded`, `payment.refunded` |

`payload.accountId` is the sender on a send and the recipient on a receive. An event says something changed: read `GET /v2/payments/{paymentId}` for what is true. Meanings and signature verification are on [Webhooks](/webhooks/event-catalog#payment-events).

## Related

<CardGroup cols={2}>
  <Card title="Depositing funds" icon="arrow-down-to-line" href="/payments/depositing-funds">
    Collect a receive payment through hosted or custom checkout.
  </Card>

  <Card title="Sending payments" icon="paper-plane" href="/payments/sending-payments">
    Preview, create and confirm a send to any destination.
  </Card>

  <Card title="Reconciliation" icon="scale-balanced" href="/payments/reconciliation">
    Match payments to your records through duplicates and reordering.
  </Card>

  <Card title="Status codes" icon="signal" href="/status-codes">
    Every status and operational reason code.
  </Card>
</CardGroup>


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