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

# Reconciliation

> Match Stableyard payments to your records and survive duplicate, late and out-of-order events.

Reconciliation proves that what you recorded matches what moved. Several signals look like an answer; only `GET /v2/payments/{paymentId}` is one.

## Credit on verified receipt, never on a report

Value counts once Stableyard has independently verified the expected receipt. That is what `accepted` and `succeeded` mean, and nothing earlier means it.

| Signal | What it proves | What it does not prove |
| - | - | - |
| `GET /v2/payments/{paymentId}` | The payment's current financial and operational state | It is the answer |
| A webhook delivery | An event happened at some point | What is true now |
| A provider success response | The rail believes it acted | That funds arrived |
| A transaction hash a payer sends you | A transaction exists | That it funded this payment |
| Your own `200` on a create | A payment resource exists | That money moved |
| A timeout or a `5xx` on your call | Nothing at all | That the operation did not happen |

The last row is the expensive one: on a timeout, read the payment, and do not create a second one. Feed reads from both webhook events and a scheduled sweep. Events keep latency low; the sweep catches deliveries that never arrived.

## Key both sides

Set `externalReference` when you create the payment, and store the returned `payment_` id in the same write.

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

`externalReference` takes up to 256 characters and is matched exactly. If you lose the payment id, it finds the payment.

```bash theme={null}
curl -G https://staging-api-v2.stableyard.fi/v2/payments \
  -u "$APP_ID:$APP_SECRET" \
  --data-urlencode "externalReference=invoice_1042"
```

Use a reference that survives a process restart, and derive your `Idempotency-Key` from the same operation; see [Idempotency](/idempotency). `metadata` is a free-form object on the payment but not a query filter, so put the identifier you will search on in `externalReference`.

## Group ledger rows by payment

`GET /v2/accounts/{accountId}/transactions` is the durable record, and one payment produces several rows: collection, settlement, fee payout and refund are separate legs. Every row carries `paymentId` and a `financialLegKind`, so group by `paymentId`; matching one row to one payment loses the rest. The same row can read as `sent` for one account and `received` for another, so label it with `perspective.direction` for the account you asked about.

## Store these fields

| Field | Why you need it |
| - | - |
| The payment `id` | The identifier every support conversation starts with |
| Your `externalReference` | Your side of the mapping |
| `status` | The financial result. See [Status codes](/status-codes) |
| `statusVersion` | Whether a read is newer than the one you already hold |
| `operationalState` and `operationalReasonCode` | Operational health, recorded separately |
| `refundSummary` | Returns you or your customer asked for |
| `incidentRecoverySummary` | Value returned because something went wrong |
| `apiVersion` | Which contract shaped what you stored |
| The last processed webhook event id | Deduplication |
| The on-chain transaction hash, where there is one | Evidence a counterparty can check independently |

<Warning>
  One column cannot hold both the financial result and the operational health. Keep two. A payment can be `succeeded` and `requires_intervention` at the same time, and overwriting one with the other loses the fact you will need.
</Warning>

## Know what sits beside status

`status` and `operationalState` are the two axes to act on. Three more fields sit beside them, and none substitutes for `status`.

| Field | What it is | Rule |
| - | - | - |
| `stage` | Diagnostic progress, from `escrow_provisioning` and `awaiting_payment` through `receipt_verifying`, `settlement_confirming` and `payout_confirming` | Nullable, and it moves backwards. Nothing customer-facing should read it |
| `offrampStatus` | The external payout result: `not_started`, `processing`, `failed`, `refunding`, `refunded`, `succeeded`. Null on every other payment type | A payment can truthfully be `accepted` while `offrampStatus` is `failed`: the source funds were verified and the fiat leg did not complete |
| `statusVersion` | Travels with the financial status | Store it with your copy of the payment to tell a stale read from a current one |

A financial status never regresses after `accepted`. When settlement, a fee payout or a refund fails afterwards, the result stands and `operationalState` moves instead.

## Survive duplicates and reordering

Delivery is at-least-once and ordering is not guaranteed. You want all three defences.

<Steps>
  <Step title="Dedupe on the event id">
    Record `x-stableyard-event-id` under a unique constraint, inside the same transaction as the business update. If the insert conflicts, return `2xx` and do nothing else.
  </Step>

  <Step title="Derive the effect from a read">
    If your handler acts on a fresh `GET` rather than on the event body, a duplicate or reordered delivery converges on the same answer instead of corrupting it.
  </Step>

  <Step title="Compare statusVersion">
    `statusVersion` increases by one whenever `status`, `stage`, `operationalState` or `operationalReasonCode` changes. Keep the highest you have seen and discard a read that reports a lower one.
  </Step>
</Steps>

Headers are on [Webhooks](/webhooks#what-a-delivery-contains), signature verification on [Verifying signatures](/webhooks/verifying-signatures), and retry behaviour on [Delivery and retries](/webhooks/delivery-and-retries).

## Sweep on your own schedule

Read every non-terminal payment older than your funding window, rather than waiting for a delivery that may never come.

```bash theme={null}
curl -G https://staging-api-v2.stableyard.fi/v2/payments \
  -u "$APP_ID:$APP_SECRET" \
  --data-urlencode "status=processing" \
  --data-urlencode "limit=100"
```

Page with `cursor` until `nextCursor` is null. Drop a payment from the sweep once it is terminal: a terminal result never reopens. [Status codes](/status-codes) names which values are terminal.

## Handle the money edges

| Case | What Stableyard records | What you do |
| - | - | - |
| **Duplicate payment** | Counted in `incidentRecoverySummary`. `payment.duplicate_received` fires. The winning receipt still settles | Credit once. The return of the duplicate is not a reversal of the obligation |
| **Late receipt** | Counted in `incidentRecoverySummary`. `payment.late_received` fires. The terminal result is preserved | Leave your record in its terminal state. An expired payment stays expired |
| **Underpayment** | Receipts accumulate against the obligation. The payment does not reach `accepted` at the lower figure | Credit nothing. There is no partial-credit path |
| **Expiry** | `status` becomes `expired`, which is terminal. `payment.expired` fires | Stop polling. A fresh attempt is a new payment with a new idempotency key |

## Tell a recovery from a refund

Every `Refund` carries a `reasonCode` that tells you who started it. Read it before you post anything against a customer's record.

| `reasonCode` | Started by |
| - | - |
| `customer_request`, `operational` | You, through `POST /v2/payments/{paymentId}/refunds` |
| `duplicate_payment`, `late_payment`, `overpayment`, `underpayment`, `offramp_provider_terminal_failure` | Stableyard, as a recovery |

On the payment, `refundSummary` aggregates the attached `Refund` resources as `status`, `count`, `requestedAmountAtomic` and `confirmedAmountAtomic`; each `refund_` resource stays the record of its own operation. `incidentRecoverySummary` covers duplicate, late, overpaid and underpaid receipts, and its amounts never contribute to `refundSummary`. That is what stops the return of an accidental duplicate from making a completed payment look refunded.

<Card title="Next: Error handling" icon="triangle-exclamation" href="/payments/error-handling">
  What fails in a payment flow, and what to do about each case.
</Card>


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