API Gateway
Payment lifecycle
A payment is created once, signed once by the buyer, and then driven by operations — each one a transaction to the rail0 contract.
authorized to captured; a partly captured payment that is fully
refunded or released ends refunded.
authorize— card-style: funds move into the rail0 escrow, and the merchant captures some or all of them beforeauthorization_expiry. Whatever is left uncaptured goes back to the buyer.charge— one step: funds go straight to the merchant.
Operations
Section titled “Operations”| Operation | Who | What it does |
|---|---|---|
authorize | payee | Pulls the buyer’s signed funds into escrow. |
charge | payee | Pays the merchant directly, no escrow. |
capture | payee | Releases escrowed funds to the merchant; partial captures allowed. Takes an amount. |
void | payee | Returns an untouched authorization to the buyer — “cancel the hold”. Impossible once anything is captured. |
release | payer or payee | Returns the uncaptured remainder after authorization_expiry. Before then it answers 422 authorization_not_expired. |
refund | payee | Returns captured or charged funds to the buyer before refund_expiry. Takes an amount, and is two-phase (below). |
dispute / close_dispute | payer | Raise or lower the payment’s disputed flag. Signal only — no funds move or freeze. |
The operation loop
Section titled “The operation loop”Every operation runs the same three calls, so one client handles all of them:
POST /payments/{id}/{operation}/prepare— the gateway builds the unsigned EIP-1559 transaction and records apendingtransaction.- Sign the transaction with the acting party’s wallet.
POST /payments/{id}/{operation}withsigned_transaction— the gateway broadcasts it and answers202. Or broadcast it yourself and report the hash toPOST /payments/{id}/{operation}/submitted.
The gateway follows the transaction pending → submitting → submitted → confirmed (or
failed). Only a confirmed transaction moves the payment; a failed one records
last_error_code on the payment and leaves its state alone, so you can prepare again.
The submitted transaction must be the one prepare built — same to, data, value and
chain — or it is refused with 422 transaction_mismatch. Nonce and fees stay the wallet’s.
Refunds are two-phase
Section titled “Refunds are two-phase”refund moves money from the merchant back to the buyer, so the merchant signs an EIP-3009
authorization of its own first:
POST /payments/{id}/refund/preparewith anamountand nosignaturereturns asigning_payload— typed data for the payee to sign off-chain.- Call it again with the same
amountand thesignature: now it builds the transaction, and the loop continues as usual.
Knowing what is in progress
Section titled “Knowing what is in progress”Every payment carries in_flight: true while one of its transactions is on its way to
the chain. While it is set, the balances are about to move — disable further actions in
your UI. capturable_amount and refundable_amount are the authoritative residuals; the
status is a happy-path label (a payment captured in full and partially refunded still
reads captured).
A payment’s description is gateway-side only, in nothing signed on chain, so either
participant may change it at any time with
PATCH /payments/{id}. It moves no money and fires
no webhook.
Dispute reasons
Section titled “Dispute reasons”dispute and close_dispute carry an optional reason. On chain it is an opaque
bytes32; the gateway maps it to a fixed list of codes, and the bytes32 is always
keccak256("rail0.dispute.<code>"). Pass the code (or exactly its bytes32) to
dispute/prepare or
dispute/close/prepare, or omit it for no
reason. Anything outside the matching list is 422 unknown_dispute_reason.
| Opening a dispute | Meaning |
|---|---|
not_received | Goods or service not received |
not_as_described | Not as described, or not what was ordered |
damaged_or_defective | Arrived damaged or defective |
duplicate | Charged twice for the same purchase |
incorrect_amount | Amount differs from what was agreed |
cancelled | Order or subscription cancelled but still charged |
refund_not_received | A refund promised by the merchant never arrived |
unauthorized | Payment not recognised (e.g. a compromised wallet) |
other | Other reason, detailed off-chain |
| Closing a dispute | Meaning |
|---|---|
resolved_with_merchant | Resolved with the merchant off-chain |
withdrawn | Withdrawn by the buyer (a mistake, or changed their mind) |
item_received | The disputed goods or service arrived after all |
other | Other reason |
full_refund | Closed automatically by a full refund (set by the protocol, never accepted from a caller) |
Dispute responses keep the on-chain reason and close_reason and add reason_code,
reason_description, close_reason_code and close_reason_description. No reason reads
as code null, "No reason given"; a bytes32 outside the list (someone called the
contract directly) reads as code null, "Unrecognised reason". The lists also ship in
the OpenAPI spec as the DisputeOpenReason, DisputeCloseReason and
DisputeSystemCloseReason enums, with their bytes32 values, if you want to generate
constants.
Status reference
Section titled “Status reference”| Status | Meaning |
|---|---|
unsigned | Created; waiting for the buyer’s signature. |
signed | Buyer signed; waiting for authorize or charge. |
authorized | Funds in escrow, nothing captured yet. |
partially_captured | Some captured, escrow remains. |
captured | Escrow fully captured. |
charged | Paid directly in charge mode. |
expired | Authorization window lapsed uncaptured. void, release still apply. |
voided | Hold cancelled before any capture. Final. |
released | Uncaptured escrow returned after expiry. Final. |
refunded | Everything settled back. Final. |
disputed is a separate boolean, not a status.