Payments
A session states an obligation. Everything after it is measured against that statement rather than inferred from chain traffic.
The payment surface is served on both api.stonereason.com and
pay.stonereason.com. The OpenAPI 3.1 description is at
GET /v1/openapi.json.
Open a session
curl -X POST https://api.stonereason.com/v1/payments/sessions \
-H "authorization: Bearer $XRPC_KEY" \
-H 'content-type: application/json' \
-d '{
"merchant_reference": "INV-2026-0912",
"network": "base",
"asset": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
"asset_decimals": 6,
"expected_amount": "12.50",
"destination": "0x…your address…"
}'
Required fields
| Field | Notes |
|---|---|
merchant_reference |
Your own reference. XRPC never parses it. Max 200 characters. |
network |
One of the networks this deployment follows. |
asset |
A contract address, a mint, or native. Never a
ticker — USDT names a different contract on every network
and more than one on some. |
expected_amount |
As a person writes it: "12.50". More decimal places than the
asset has is refused rather than rounded. |
destination |
Where the money goes — your address. On networks where an address alone does
not identify the payee, routing is also required. |
Optional, and worth knowing
asset_decimals— required for an asset whose scale XRPC has not established. Without it the session is refused withDECIMALS_UNKNOWN, because an amount whose scale is unknown cannot be read.amount_match_policy—ExactorAtLeast.AnyPositiveis refused: a session that cannot be underpaid is not an obligation.idempotency_key— the same key with the same request returns the same session; with a materially different request it is a 409.expires_in_seconds— how long your offer stands. This is not how long XRPC watches.metadata— up to 1 KB, opaque, echoed on every delivery.
The creation floor
The response carries the chain head read before the session existed:
"creation": {
"chain_head": 51184176,
"creation_floor": 51184176
}
A payment at or below the floor was on the chain before your offer existed and cannot pay for it. The floor is the maximum of what has been delivered, what the ingest has read, and the chain head — three positions that are never conflated. A delivery or webhook cursor is never used as a chain head.
If a network cannot establish a chain head safely, session creation on it is refused rather than given an invented floor.
Reconciliation: four standings
"reconciliation": {
"amount": { "standing": "NOTHING_SEEN" },
"finality": { "standing": "NOTHING_TO_SETTLE" },
"observation": { "standing": "ESTABLISHED", "read_to_height": 51184180 },
"timing": { "standing": "NO_DEADLINE" }
}
- amount —
NOTHING_SEEN,UNDERPAID,EXPECTED_AMOUNT_SEEN,OVERPAID. - finality — settlement in the chain's own vocabulary.
- observation — whether XRPC is watching and to what height. If
this is not
ESTABLISHED, silence means nothing. - timing — whether your offer still stands.
may_release_goods combines all four. It is the only field that
answers the merchant's real question, and it is false whenever part of the picture
is missing.
Reading a session
| Endpoint | Returns |
|---|---|
GET /v1/payments/sessions |
Your sessions, scoped to the calling project. |
GET /v1/payments/sessions/{id} |
The session, its reconciliation and may_release_goods. |
GET /v1/payments/sessions/{id}/events |
Every step of every payment for this session. |
GET /v1/payments/sessions/{id}/receipt |
The evidence receipt. |
POST /v1/payments/sessions/{id}/cancel |
Withdraws the offer. Lifecycle becomes CANCELLED. |
The evidence receipt
A receipt records what XRPC read, each entry named in the chain's own identity. It also states, in its own body, what it is not:
"basis": {
"standing": "XRPC_OBSERVATION",
"why_not_a_proof": "this is XRPC's record of what it read from the chain,
not a proof from the chain. It carries no validator signature and no
inclusion proof, and it cannot be checked by anybody who does not trust
XRPC. To verify independently, take the native identity of each entry to
any node or explorer for that network"
}
That sentence is the point of the receipt. It gives you everything needed to check the claim somewhere else.
Webhooks
- Register destinations with
POST /v1/webhooks. Up to 10 per project. - Deliveries are signed HMAC-SHA256. Verify the signature before trusting the body.
- Delivery ids do not repeat, and resume survives a restart — watches are re-registered from their own creation height.
- A webhook is a notification, not a source of truth. Re-read the session; the delivered position is not a chain position.
Free Beta limits
Reported by GET /v1/payments/beta: 1,000 watches per project, 200
active, 10 webhook destinations, light-path pass every 12 seconds with up to 200
sessions per pass.