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 whataccepted and succeeded mean, and nothing earlier means it.
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
SetexternalReference when you create the payment, and store the returned payment_ id in the same write.
externalReference takes up to 256 characters and is matched exactly. If you lose the payment id, it finds the payment.
Idempotency-Key from the same operation; see 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
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.
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.1
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.2
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.3
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.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.cursor until nextCursor is null. Drop a payment from the sweep once it is terminal: a terminal result never reopens. Status codes names which values are terminal.
Handle the money edges
Tell a recovery from a refund
EveryRefund carries a reasonCode that tells you who started it. Read it before you post anything against a customer’s record.
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.
Next: Error handling
What fails in a payment flow, and what to do about each case.