Skip to content

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.

  • authorize — card-style: funds move into the rail0 escrow, and the merchant captures some or all of them before authorization_expiry. Whatever is left uncaptured goes back to the buyer.
  • charge — one step: funds go straight to the merchant.
OperationWhoWhat it does
authorizepayeePulls the buyer’s signed funds into escrow.
chargepayeePays the merchant directly, no escrow.
capturepayeeReleases escrowed funds to the merchant; partial captures allowed. Takes an amount.
voidpayeeReturns an untouched authorization to the buyer — “cancel the hold”. Impossible once anything is captured.
releasepayer or payeeReturns the uncaptured remainder after authorization_expiry. Before then it answers 422 authorization_not_expired.
refundpayeeReturns captured or charged funds to the buyer before refund_expiry. Takes an amount, and is two-phase (below).
dispute / close_disputepayerRaise or lower the payment’s disputed flag. Signal only — no funds move or freeze.

Every operation runs the same three calls, so one client handles all of them:

  1. POST /payments/{id}/{operation}/prepare — the gateway builds the unsigned EIP-1559 transaction and records a pending transaction.
  2. Sign the transaction with the acting party’s wallet.
  3. POST /payments/{id}/{operation} with signed_transaction — the gateway broadcasts it and answers 202. Or broadcast it yourself and report the hash to POST /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.

refund moves money from the merchant back to the buyer, so the merchant signs an EIP-3009 authorization of its own first:

  1. POST /payments/{id}/refund/prepare with an amount and no signature returns a signing_payload — typed data for the payee to sign off-chain.
  2. Call it again with the same amount and the signature: now it builds the transaction, and the loop continues as usual.

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 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 disputeMeaning
not_receivedGoods or service not received
not_as_describedNot as described, or not what was ordered
damaged_or_defectiveArrived damaged or defective
duplicateCharged twice for the same purchase
incorrect_amountAmount differs from what was agreed
cancelledOrder or subscription cancelled but still charged
refund_not_receivedA refund promised by the merchant never arrived
unauthorizedPayment not recognised (e.g. a compromised wallet)
otherOther reason, detailed off-chain
Closing a disputeMeaning
resolved_with_merchantResolved with the merchant off-chain
withdrawnWithdrawn by the buyer (a mistake, or changed their mind)
item_receivedThe disputed goods or service arrived after all
otherOther reason
full_refundClosed 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.

StatusMeaning
unsignedCreated; waiting for the buyer’s signature.
signedBuyer signed; waiting for authorize or charge.
authorizedFunds in escrow, nothing captured yet.
partially_capturedSome captured, escrow remains.
capturedEscrow fully captured.
chargedPaid directly in charge mode.
expiredAuthorization window lapsed uncaptured. void, release still apply.
voidedHold cancelled before any capture. Final.
releasedUncaptured escrow returned after expiry. Final.
refundedEverything settled back. Final.

disputed is a separate boolean, not a status.