Skip to content

API reference

Payments

Payment lifecycle: create, sign, prepare/submit per operation.

List payments for the authenticated wallet (payer or payee)

GET /payments

Bearer JWT (Sign-In With Ethereum)

Query parameters

  • page integer

    1-based page number.

  • per_page integer

    Items per page (capped at 100).

  • after string

    Cursor paging: the x-next-cursor (or the Link next's after) of the previous page. Walks the collection by -created_at without a total count, at a constant cost per page; page and any sort other than -created_at are rejected with 422 invalid_cursor beside it. The last page carries no x-next-cursor and no next link.

  • sort string

    Comma-separated sort fields; prefix with - for descending (e.g. -created_at,status). An unsupported field is rejected with 422 invalid_sort, whose detail names the allowed set — it is never silently dropped.

  • status array of string

    Lifecycle state(s): one value, a comma-separated list (status=authorized,expired) or the repeated status[]= form; matches any of them. An unknown value is 400 validation_failed naming it. Deliberately a HAPPY-PATH label, not a ledger: a partial operation does NOT move it, so a payment captured 100 and refunded 40 still reads captured, and a full refund that leaves uncaptured escrow does too. capturable_amount / refundable_amount are the authoritative residuals — reconcile on those, not on this. partially_refunded is retained for historical rows only and is no longer produced.

    One of: unsigned, signed, authorized, charged, captured, partially_captured, expired, voided, released, refunded, partially_refunded
  • mode string
    One of: authorize, charge
  • payer string
  • payee string
  • token string
  • rail0_id string

    Filter by the logical on-chain payment id (0x…).

  • chain_id integer

    Filter by the payment's chain (resolved via its contract).

  • disputed boolean

    Filter by whether an open dispute exists.

  • min_amount string

    Minimum amount in token base units (inclusive).

  • max_amount string

    Maximum amount in token base units (inclusive).

  • created_from string (date-time)

    Only payments created at/after this time (ISO-8601).

  • created_to string (date-time)

    Only payments created at/before this time (ISO-8601).

  • operation string

    Only payments carrying at least one transaction with this operation.

    One of: authorize, capture, charge, void, release, refund, dispute, close_dispute
  • scope string

    mine (default): the authenticated wallet's payments. all (admin only, 403 otherwise): every payment on the gateway.

    One of: mine, all
  • account_id string

    scope=all only: payments whose payee is one of this account's wallets.

  • has_error boolean

    scope=all only: true = payments carrying a last_error_code; false = none.

  • expiring_within integer

    scope=all only: capturable holds (authorized, or partially captured with escrow still held) whose authorization_expiry falls within this many seconds from now and has not yet passed.

Responses

  • 200 Payments.
  • 401 Authentication failed.
Terminal window
curl "https://api.rail0.xyz/payments?status=authorized&sort=-created_at" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
[
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"contract_id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"chain_id": 84532,
"rail0_id": "0x9c2f0d4e7b1a3c5e8f6d2b4a1c3e5f7d9b0a2c4e6f8d1b3a5c7e9f0d2b4a6c8e",
"status": "authorized",
"mode": "authorize",
"amount": "10500000",
"capturable_amount": "10500000",
"refundable_amount": "10500000",
"config_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"payer": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"payee": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"decimals": 6,
"authorization_expiry": 1791633600,
"refund_expiry": 1794225600,
"escrow_stranded": false,
"escrow_returnable_at": null,
"disputed": false,
"in_flight": false,
"last_error_code": null,
"last_error_message": null,
"description": "Order #1042",
"metadata": {
"order_id": "ord_1042"
},
"signed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}
]

Create a payment

POST /payments

Bearer JWT (Sign-In With Ethereum)

Session required (an account-less token is fine); the payer in the body must equal the authenticated address (403 otherwise) — a buyer only drafts payments payable from its own wallet. payer and payee must be different addresses (a payment to oneself is rejected with 422). Idempotent on the Idempotency-Key header, scoped to the caller: when a payment already exists for the key AND the same payer, that record is returned with 200 instead of 201. Keys are per payer, uniqueness included: a key another payer already used is not shared and does not conflict — this payer's request creates its own payment (201), and never returns or reveals the other one.

Idempotency-Key (optional header) REJECTS a reused key rather than replaying it: the same key with different terms answers 422 idempotency_key_reused, not 200 carrying the first payment — silently returning a payment whose terms are not the ones just sent is how a client charges the wrong amount. Same key + same terms replays with 200 (amounts compare converted, so "1.0" and "1.00" match). A key used by another payer is independent of this payer's: it neither replays nor conflicts. Rotate per logical order.

Headers

  • Idempotency-Key string

    Client-supplied key; replays return the existing payment.

Request body

  • chain_id integer required

    EVM chain id of the target deployment.

  • mode string required
    One of: authorize, charge
  • amount string required

    Human decimal amount, e.g. "10.50".

  • token string required

    Token address (0x, 40 hex).

  • payer string required

    Buyer address (0x, 40 hex).

  • payee string required

    Merchant address (0x, 40 hex).

  • description string

    Free-text note (what is being paid for, a reconciliation reference). Gateway-side only: in nothing signed on chain, editable afterwards via PATCH /payments/{id}, and not an idempotency term.

  • metadata object

Responses

  • 200 Existing payment returned (idempotent replay).
  • 201 Payment created.
  • 422 Validation error. code is one of no_active_contract, unknown_token, unsupported_payment_method (the payee does not offer that token/chain), invalid_amount, idempotency_key_reused (the Idempotency-Key was already used by this payer for a payment with DIFFERENT terms — retry the original request with that key, or use a new key for the new terms; the response carries the existing payment_id).
Terminal window
curl -X POST "https://api.rail0.xyz/payments" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H 'Idempotency-Key: ord_1042-create' \
-H "Content-Type: application/json" \
-d '{
"chain_id": 84532,
"mode": "authorize",
"amount": "10.50",
"token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"payer": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"payee": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"metadata": {
"order_id": "ord_1042"
}
}'
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"contract_id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"chain_id": 84532,
"rail0_id": "0x9c2f0d4e7b1a3c5e8f6d2b4a1c3e5f7d9b0a2c4e6f8d1b3a5c7e9f0d2b4a6c8e",
"status": "authorized",
"mode": "authorize",
"amount": "10500000",
"capturable_amount": "10500000",
"refundable_amount": "10500000",
"config_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"payer": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"payee": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"decimals": 6,
"authorization_expiry": 1791633600,
"refund_expiry": 1794225600,
"escrow_stranded": false,
"escrow_returnable_at": null,
"disputed": false,
"in_flight": false,
"last_error_code": null,
"last_error_message": null,
"description": "Order #1042",
"metadata": {
"order_id": "ord_1042"
},
"signed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z",
"rail0_contract": "0xE6E694Dc786Dc8a43152a4547b56A2433e7e9B1c",
"transactions": [
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}
],
"signing_payload": null
}

Get a payment with embedded transactions and optional signing payload

GET /payments/{id}

Bearer JWT (Sign-In With Ethereum)

Participant-only: readable only by the payment's payer or payee (bearerAuth), not by anyone who learns the id. The payer authenticates with an account-less SIWE token. 401 without a token, 403 for a non-participant. An active admin reads ANY payment through the Full entity: everything a participant reads (chain_id, rail0_contract, the escrow fields, the embedded transactions, and signing_payload while unsigned) plus every column — signatures, broadcast bookkeeping, error diagnostics. The operator's debugging view, a superset of the participant's, so an admin who is party to the payment can act on it.

Path parameters

  • id string (uuid) required

    The :id accepts either the payment UUID or its rail0_id (the contract's bytes32 paymentId, 0x…), resolved to the UUID internally. This holds for every /payments/{id}/… route.

Responses

  • 200 Payment detail.
  • 401 Authentication failed.
  • 404 Resource not found.
Terminal window
curl "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"contract_id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"chain_id": 84532,
"rail0_id": "0x9c2f0d4e7b1a3c5e8f6d2b4a1c3e5f7d9b0a2c4e6f8d1b3a5c7e9f0d2b4a6c8e",
"status": "authorized",
"mode": "authorize",
"amount": "10500000",
"capturable_amount": "10500000",
"refundable_amount": "10500000",
"config_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"payer": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"payee": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"decimals": 6,
"authorization_expiry": 1791633600,
"refund_expiry": 1794225600,
"escrow_stranded": false,
"escrow_returnable_at": null,
"disputed": false,
"in_flight": false,
"last_error_code": null,
"last_error_message": null,
"description": "Order #1042",
"metadata": {
"order_id": "ord_1042"
},
"signed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z",
"rail0_contract": "0xE6E694Dc786Dc8a43152a4547b56A2433e7e9B1c",
"transactions": [
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}
],
"signing_payload": null
}

Update a payment's description

PATCH /payments/{id}

Bearer JWT (Sign-In With Ethereum)

Participant-only (payer or payee): 401 without a token, 404 for a non-participant, 403 wallet_deactivated from a retired wallet. Allowed in every status, closed ones included. The description is gateway-side only — in nothing signed or hashed on chain — so this moves no money and dispatches no webhook. A changed value bumps updated_at; writing the same value back is a no-op.

Path parameters

  • id string (uuid) required

    The :id accepts either the payment UUID or its rail0_id (the contract's bytes32 paymentId, 0x…), resolved to the UUID internally. This holds for every /payments/{id}/… route.

Request body

  • description string | null required

    New description; null or an empty string clears it.

Responses

  • 200 Payment updated.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 Validation error.
Terminal window
curl -X PATCH "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"description": "Order #1042"
}'
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"contract_id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"chain_id": 84532,
"rail0_id": "0x9c2f0d4e7b1a3c5e8f6d2b4a1c3e5f7d9b0a2c4e6f8d1b3a5c7e9f0d2b4a6c8e",
"status": "authorized",
"mode": "authorize",
"amount": "10500000",
"capturable_amount": "10500000",
"refundable_amount": "10500000",
"config_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"payer": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"payee": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"decimals": 6,
"authorization_expiry": 1791633600,
"refund_expiry": 1794225600,
"escrow_stranded": false,
"escrow_returnable_at": null,
"disputed": false,
"in_flight": false,
"last_error_code": null,
"last_error_message": null,
"description": "Order #1042",
"metadata": {
"order_id": "ord_1042"
},
"signed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z",
"rail0_contract": "0xE6E694Dc786Dc8a43152a4547b56A2433e7e9B1c",
"transactions": [
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}
],
"signing_payload": null
}

Store the payer's signature on a payment

PUT /payments/{id}/sign

Bearer JWT (Sign-In With Ethereum)

Session required (an account-less token is fine) and the caller must be a participant (payer or payee). The signer itself is additionally verified from the recovered signature, not the session party.

Path parameters

  • id string (uuid) required

Request body

  • signature string required

    Payer's EIP-3009 signature (0x…, 65 bytes).

Responses

  • 200 Signature stored. Returns the updated payment, now in the signed state.
  • 401 Authentication failed.
  • 404 Resource not found.
  • 422 Payment not signable, signer mismatch, or invalid_signature: a signature that cannot be recovered, or whose recovery byte v is not 27/28 (0/1 are read as 27/28).
Terminal window
curl -X PUT "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/sign" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"signature": "0x6b1f…9c1b"
}'
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"contract_id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"chain_id": 84532,
"rail0_id": "0x9c2f0d4e7b1a3c5e8f6d2b4a1c3e5f7d9b0a2c4e6f8d1b3a5c7e9f0d2b4a6c8e",
"status": "authorized",
"mode": "authorize",
"amount": "10500000",
"capturable_amount": "10500000",
"refundable_amount": "10500000",
"config_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"payer": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"payee": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"decimals": 6,
"authorization_expiry": 1791633600,
"refund_expiry": 1794225600,
"escrow_stranded": false,
"escrow_returnable_at": null,
"disputed": false,
"in_flight": false,
"last_error_code": null,
"last_error_message": null,
"description": "Order #1042",
"metadata": {
"order_id": "ord_1042"
},
"signed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z",
"rail0_contract": "0xE6E694Dc786Dc8a43152a4547b56A2433e7e9B1c",
"transactions": [
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}
],
"signing_payload": null
}

List a payment's transactions

GET /payments/{id}/transactions

Bearer JWT (Sign-In With Ethereum)

Participant-only (bearerAuth): readable only by the payment's payer or payee. 401 without a token, 403 for a non-participant.

Path parameters

  • id string (uuid) required

Query parameters

  • page integer

    1-based page number.

  • per_page integer

    Items per page (capped at 100).

  • sort string

    Comma-separated sort fields; prefix with - for descending (e.g. -created_at,status). An unsupported field is rejected with 422 invalid_sort, whose detail names the allowed set — it is never silently dropped.

  • operation string
    One of: authorize, capture, charge, void, release, refund, dispute, close_dispute
  • status array of string

    Transaction status(es): one value, a comma-separated list or the repeated status[]= form; matches any of them.

    One of: pending, submitting, submitted, confirmed, failed

Responses

  • 200 Transactions.
  • 401 Authentication failed.
  • 404 Resource not found.
Terminal window
curl "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/transactions" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
[
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}
]

Fetch one of a payment's transactions

GET /payments/{id}/transactions/{transaction_id}

Bearer JWT (Sign-In With Ethereum)

Reads a single transaction by id - the read the list endpoint could only answer by returning every row. Anything holding a transaction id (the action_id an asynchronous integration is handed when an operation is accepted) resolves it directly, instead of fetching the payment and scanning its transactions for an id it already has. Participant-only (bearerAuth): readable by the payment's payer or payee. A caller who is party to neither gets 404 rather than 403, so the response never confirms the payment exists; an unknown, malformed or foreign transaction id answers 404 the same way.

Path parameters

  • id string (uuid) required
  • transaction_id string (uuid) required

    The transaction row to read, resolved through the payment's own transactions - an id belonging to another payment answers 404.

Responses

  • 200 The transaction.
  • 401 Authentication failed.
  • 404 Resource not found.
Terminal window
curl "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/transactions/7e3f1a2b-6c5d-4e8f-9a0b-1c2d3e4f5a6b" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Re-enqueue a stuck broadcast for this transaction (payee, or an operator)

POST /payments/{id}/transactions/{transaction_id}/redrive

Bearer JWT (Sign-In With Ethereum)

Payee-gated, OR an operator session on any payment (audited as transaction.redrive). On-demand doorbell for the recovery the Recovery already performs at cadence: a pending transaction still holding its signed_transaction is a broadcast the queue lost (a hard worker kill drops the in-flight job), and this re-enqueues the broadcaster for it NOW instead of waiting for the recovery tick. Same redrivable predicate, same enqueue, no new state and no new worker. Payee-only, like the other operation writes. Idempotent under double-click: the broadcaster re-checks under the row lock before sending, so a duplicate enqueue is harmless and never double-broadcasts. Any other state is refused with 422 not_redrivable, the detail naming the state the row IS in (a pending row without its signed transaction has nothing to broadcast; submitting/submitted already reached the send; confirmed is settled; a failed row's repair path is the operation's /submitted endpoint, not another send).

Path parameters

  • id string (uuid) required
  • transaction_id string (uuid) required

    The transaction row to redrive, resolved through the payment's own transactions - an id belonging to another payment answers 404.

Responses

  • 202 Accepted; the broadcaster was re-enqueued and the broadcast happens asynchronously.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 The transaction is not in the redrivable state (pending with its signed_transaction stored). Code not_redrivable; the detail names the state the row is in.
  • 503 queue_unavailable: the job queue could not be reached, so nothing was enqueued. The row is unchanged; retry in a moment.
Terminal window
curl -X POST "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/transactions/7e3f1a2b-6c5d-4e8f-9a0b-1c2d3e4f5a6b/redrive" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 202
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Prepare an operation's unsigned transaction

POST /payments/{id}/{operation}/prepare

Bearer JWT (Sign-In With Ethereum)

Builds the unsigned transaction and stores it on a new pending transaction. Session required (bearerAuth) for every operation. The payee operations additionally require the caller to be the payee; release is not payee-gated (it is payer-or-payee on-chain and the payer has no gateway account, so its acting party is authorized on-chain — the signed tx + the contract's NotPayerOrPayee gate) but its prepare is still restricted to a payment participant. Each operation reads only its own body fields: amount for capture and refund (required for both), signature for refund, from for release; authorize, charge and void take none. A field another operation reads is ignored, never an error. refund is two-phase: with no signature it returns { signing_payload } (phase 1, no transaction row created); with signature it creates the pending transaction (phase 2). release is also the one operation with an INVERTED time window: it returns the uncaptured remainder of an authorization, which the contract keeps available to the merchant until the authorization expires, so release opens only AFTER authorization_expiry and answers 422 authorization_not_expired before then (the refusal carries the timestamp). The operation whose card-processing analogue is “cancel the hold early” is void, not release — and void requires the authorization to be fully intact, so any capture rules it out permanently.

Path parameters

  • id string (uuid) required
  • operation string required

    Payment operation namespace. These six are the operations reachable through the generic namespace; dispute/close_dispute are payer-only and have their own dedicated routes. The set of operations a stored transaction can CARRY is wider — see Transaction.operation.

    One of: authorize, capture, charge, void, release, refund

Headers

  • Idempotency-Key string

    Client-supplied key; a replay returns the transaction the first call created, with 200 instead of 201. Bound to the request as well as the key: the same key with different terms (a corrected amount, another dispute reason) is refused 422 idempotency_key_reused rather than answered with the first transaction. Scoped to this payment. Without it a retry that arrives after the first transaction was signed and broadcast opens a SECOND transaction, which is the correct behaviour for a genuine sequential partial capture and the wrong one for a retry - only the caller can tell those apart.

Request body (optional)

  • amount string

    capture and refund only (required there): the amount as a human decimal. Ignored by the other operations.

  • signature string

    refund only: the payee's EIP-3009 refund signature (phase 2; 0x + 130 hex). Ignored by the other operations.

  • from string

    release only: the submitter, the payment's payer or payee (422 release_submitter_not_a_party otherwise). The transaction is built with its nonce. Defaults to the caller. Ignored by the other operations.

Responses

  • 200 Reused the existing pending transaction (idempotent re-prepare); or, for refund phase-1, the { signing_payload } for the payee to sign off-chain (no transaction created).
  • 201 Created the pending (unsubmitted) transaction.
  • 400 Missing required param (amount for capture/refund, signature for refund phase-2).
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 Payment not in a state that permits this operation. Inside the stranded-escrow window (after a partial capture, before authorization_expiry) the void and release refusals each name the other verb and the timestamp when release opens — the codes (not_voidable / authorization_not_expired) are unchanged and remain the field to branch on.
Terminal window
curl -X POST "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/capture/prepare" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H 'Idempotency-Key: ord_1042-capture-1' \
-H "Content-Type: application/json" \
-d '{
"amount": "10.50"
}'
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Submit an operation's signed transaction

POST /payments/{id}/{operation}

Bearer JWT (Sign-In With Ethereum)

Stores the caller's signed raw transaction on the latest pending transaction for this operation and enqueues the broadcaster. Session required (bearerAuth) for every operation. The payee operations additionally require the caller to be the payee; release accepts any session (payer-or-payee submit it). As a robustness check the gateway also recovers the signed tx's sender and requires it to match the party the contract accepts as msg.sender — the payee for payee ops, the payer or payee for release — returning 403 otherwise and 400 if the tx can't be decoded, before broadcasting. Broadcast happens asynchronously.

Path parameters

  • id string (uuid) required
  • operation string required

    Payment operation namespace. These six are the operations reachable through the generic namespace; dispute/close_dispute are payer-only and have their own dedicated routes. The set of operations a stored transaction can CARRY is wider — see Transaction.operation.

    One of: authorize, capture, charge, void, release, refund

Request body

  • signed_transaction string required

    Signed raw transaction (0x…).

Responses

  • 202 Accepted; broadcast enqueued.
  • 400 The signed transaction could not be decoded.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 transaction_mismatch: the signed transaction's target, calldata, value or chain differs from the one this operation prepared. Sign the payload prepare returned, unmodified.
Terminal window
curl -X POST "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/capture" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"signed_transaction": "0x02f8b1…"
}'
Response 202
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Record an externally-broadcast operation transaction (MetaMask)

POST /payments/{id}/{operation}/submitted

Bearer JWT (Sign-In With Ethereum)

For wallets that sign and broadcast in one step (MetaMask eth_sendTransaction) and so cannot hand the gateway a raw signed_transaction. The caller broadcasts the prepared transaction themselves and reports its hash here; the gateway VERIFIES on-chain that the transaction targets this payment's RAIL0 deployment and carries this payment's id in its calldata (422 foreign_transaction_hash otherwise — a hash no node has seen yet is accepted, since propagation is not instant), then attaches it to the operation's open transaction and moves it straight to submitted, WITHOUT the broadcaster — the indexer then confirms it by hash exactly as for a gateway-broadcast tx. JWT-gated (bearerAuth) since a bare hash carries no signature to authorize it: payee-only for the merchant operations (authorize/capture/charge/void/refund); release is payer-or-payee on-chain so it accepts EITHER participant (a MetaMask buyer can report a payer-side release). The payer operations dispute/close_dispute have their own payer-only /submitted endpoints. Re-callable while the tx is still unconfirmed to OVERWRITE a stuck or wrong hash; a confirmed/failed operation is terminal and returns 422.

Path parameters

  • id string (uuid) required
  • operation string required

    Payment operation namespace. These six are the operations reachable through the generic namespace; dispute/close_dispute are payer-only and have their own dedicated routes. The set of operations a stored transaction can CARRY is wider — see Transaction.operation.

    One of: authorize, capture, charge, void, release, refund

Request body

  • transaction_hash string required

    Hash of the already-broadcast transaction (0x + 64 hex).

Responses

  • 202 Accepted; the hash was recorded and awaits on-chain confirmation.
  • 400 Malformed transaction_hash.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 The operation's transaction is already confirmed — its hash is not overwritable. A failed row IS overwritable: re-reporting the hash repairs an orphan (mined on-chain, recorded failed) by moving it back to submitted.
Terminal window
curl -X POST "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/capture/submitted" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd"
}'
Response 202
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Record an externally-broadcast dispute transaction (MetaMask)

POST /payments/{id}/dispute/submitted

Bearer JWT (Sign-In With Ethereum)

The payer's counterpart to /payments/{id}/{operation}/submitted: a MetaMask buyer signs+sends the dispute() tx in one step and reports its hash here. Payer-only (bearerAuth): the bare hash carries no signature, so the SIWE session is the authorization (the payer authenticates with an account-less token). Records the hash on the payment's dispute transaction and moves it to submitted, skipping the broadcaster. Re-callable while unconfirmed; a confirmed/failed tx is terminal (422).

Path parameters

  • id string (uuid) required

Request body

  • transaction_hash string required

    Hash of the already-broadcast transaction (0x + 64 hex).

Responses

  • 202 Accepted; the hash was recorded and awaits on-chain confirmation.
  • 400 Malformed transaction_hash.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 The operation's transaction is already confirmed — its hash is not overwritable. A failed row IS overwritable: re-reporting the hash repairs an orphan (mined on-chain, recorded failed) by moving it back to submitted.
Terminal window
curl -X POST "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/dispute/submitted" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd"
}'
Response 202
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Record an externally-broadcast close-dispute transaction (MetaMask)

POST /payments/{id}/dispute/close/submitted

Bearer JWT (Sign-In With Ethereum)

Payer-only report-by-hash for the closeDispute() tx, same contract as /payments/{id}/dispute/submitted. Lets a MetaMask buyer close a dispute it broadcast itself.

Path parameters

  • id string (uuid) required

Request body

  • transaction_hash string required

    Hash of the already-broadcast transaction (0x + 64 hex).

Responses

  • 202 Accepted; the hash was recorded and awaits on-chain confirmation.
  • 400 Malformed transaction_hash.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 The operation's transaction is already confirmed — its hash is not overwritable. A failed row IS overwritable: re-reporting the hash repairs an orphan (mined on-chain, recorded failed) by moving it back to submitted.
Terminal window
curl -X POST "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/dispute/close/submitted" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd"
}'
Response 202
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Prepare the dispute transaction (open; payer-signed)

POST /payments/{id}/dispute/prepare

Bearer JWT (Sign-In With Ethereum)

Builds the unsigned dispute() transaction on a pending transaction for the payer to sign. Signal-only (no fund effect). Session required (an account-less token is fine) and restricted to a payment participant (payer or payee); the acting party (payer) is authorized on-chain, not by the session (the payer has no gateway account) — the payer-signed tx + the contract's NotPayer gate. Submit the signed tx to POST /payments/{id}/dispute.

Path parameters

  • id string (uuid) required

Headers

  • Idempotency-Key string

    Client-supplied key; a replay returns the transaction the first call created, with 200 instead of 201. Bound to the request as well as the key: the same key with different terms (a corrected amount, another dispute reason) is refused 422 idempotency_key_reused rather than answered with the first transaction. Scoped to this payment. Without it a retry that arrives after the first transaction was signed and broadcast opens a SECOND transaction, which is the correct behaviour for a genuine sequential partial capture and the wrong one for a retry - only the caller can tell those apart.

Request body (optional)

  • reason string

    Optional. A DisputeOpenReason code (e.g. not_received), or exactly that code's bytes32 — keccak256("rail0.dispute.") as 0x-hex; the gateway encodes the bytes32 into the calldata. Omitted (or the all-zero bytes32) means no reason: the calldata carries bytes32 zero. Any other value is refused 422 unknown_dispute_reason.

    One of: not_received, not_as_described, damaged_or_defective, duplicate, incorrect_amount, cancelled, refund_not_received, unauthorized, other

Responses

  • 200 Reused the existing pending dispute transaction.
  • 201 Pending dispute transaction with the unsigned payload.
  • 401 Authentication failed.
  • 403 not_the_payer: only the payment's payer can prepare this; a payee or another session gets this 403 (a stranger gets 404).
  • 404 Resource not found.
  • 422 unknown_dispute_reason when reason is supplied, non-zero, and not a DisputeOpenReason code or its bytes32; otherwise the payment-state refusal or idempotency_key_reused.
Terminal window
curl -X POST "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/dispute/prepare" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Submit the signed dispute transaction (open; payer-signed)

POST /payments/{id}/dispute

Bearer JWT (Sign-In With Ethereum)

Stores the payer's signed dispute() transaction and enqueues the broadcaster. Session required (an account-less token is fine); the acting party is authorized on-chain — the gateway recovers the signed tx's sender and requires it to be the payer (403 otherwise, 400 if undecodable), mirroring the contract's NotPayer gate. The disputed flag is set when the indexer reports the on-chain event.

Path parameters

  • id string (uuid) required

Request body

  • signed_transaction string required

    Signed raw transaction (0x…).

Responses

  • 202 Accepted — broadcast happens asynchronously.
  • 400 The signed transaction could not be decoded.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 transaction_mismatch: the signed transaction's target, calldata, value or chain differs from the one this operation prepared. Sign the payload prepare returned, unmodified.
Terminal window
curl -X POST "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/dispute" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"signed_transaction": "0x02f8b1…"
}'
Response 202
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Prepare the close-dispute transaction (open; payer-signed)

POST /payments/{id}/dispute/close/prepare

Bearer JWT (Sign-In With Ethereum)

Builds the unsigned closeDispute() transaction on a pending transaction for the payer to sign. Session required (an account-less token is fine) and restricted to a payment participant (payer or payee); the acting party (payer) is authorized on-chain, not by the session (the payer has no gateway account) — the payer-signed tx + the contract's NotPayer gate. Submit the signed tx to POST /payments/{id}/dispute/close.

Path parameters

  • id string (uuid) required

Headers

  • Idempotency-Key string

    Client-supplied key; a replay returns the transaction the first call created, with 200 instead of 201. Bound to the request as well as the key: the same key with different terms (a corrected amount, another dispute reason) is refused 422 idempotency_key_reused rather than answered with the first transaction. Scoped to this payment. Without it a retry that arrives after the first transaction was signed and broadcast opens a SECOND transaction, which is the correct behaviour for a genuine sequential partial capture and the wrong one for a retry - only the caller can tell those apart.

Request body (optional)

  • reason string

    Optional. A DisputeCloseReason code (e.g. withdrawn), or exactly that code's bytes32 — keccak256("rail0.dispute.") as 0x-hex; the gateway encodes the bytes32 into the calldata. Omitted (or the all-zero bytes32) means no reason: the calldata carries bytes32 zero. full_refund is recorded by the protocol itself and is not accepted here. Any other value is refused 422 unknown_dispute_reason.

    One of: resolved_with_merchant, withdrawn, item_received, other

Responses

  • 200 Reused the existing pending close-dispute transaction.
  • 201 Pending close-dispute transaction with the unsigned payload.
  • 401 Authentication failed.
  • 403 not_the_payer: only the payment's payer can prepare this; a payee or another session gets this 403 (a stranger gets 404).
  • 404 Resource not found.
  • 422 unknown_dispute_reason when reason is supplied, non-zero, and not a DisputeCloseReason code or its bytes32; otherwise the payment-state refusal or idempotency_key_reused.
Terminal window
curl -X POST "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/dispute/close/prepare" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Submit the signed close-dispute transaction (open; payer-signed)

POST /payments/{id}/dispute/close

Bearer JWT (Sign-In With Ethereum)

Stores the payer's signed closeDispute() transaction and enqueues the broadcaster. Session required (an account-less token is fine); the acting party is authorized on-chain — the gateway recovers the signed tx's sender and requires it to be the payer (403 otherwise, 400 if undecodable), mirroring the contract's NotPayer gate.

Path parameters

  • id string (uuid) required

Request body

  • signed_transaction string required

    Signed raw transaction (0x…).

Responses

  • 202 Accepted — broadcast happens asynchronously.
  • 400 The signed transaction could not be decoded.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 transaction_mismatch: the signed transaction's target, calldata, value or chain differs from the one this operation prepared. Sign the payload prepare returned, unmodified.
Terminal window
curl -X POST "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/dispute/close" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"signed_transaction": "0x02f8b1…"
}'
Response 202
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"operation": "capture",
"status": "pending",
"redrivable": false,
"error_code": null,
"error_title": null,
"error_detail": null,
"unsigned_transaction": null,
"transaction_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
"sender": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"amount": "10500000",
"block_number": null,
"gas_used": null,
"gas_limit": null,
"effective_gas_price": null,
"base_fee_per_gas": null,
"gas_cost": null,
"pending_at": null,
"submitted_at": null,
"confirmed_at": null,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

List a payment's disputes

GET /payments/{id}/disputes

Bearer JWT (Sign-In With Ethereum)

Participant-only (bearerAuth): readable only by the payment's payer or payee. 401 without a token, 403 for a non-participant.

Path parameters

  • id string (uuid) required

Query parameters

  • page integer

    1-based page number.

  • per_page integer

    Items per page (capped at 100).

  • sort string

    Comma-separated sort fields; prefix with - for descending (e.g. -created_at,status). An unsupported field is rejected with 422 invalid_sort, whose detail names the allowed set — it is never silently dropped.

  • status string

    Filter by dispute status.

    One of: open, closed

Responses

  • 200 Dispute open/close history.
  • 401 Authentication failed.
  • 404 Resource not found.
Terminal window
curl "https://api.rail0.xyz/payments/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/disputes" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
[
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"status": "open",
"reason": "string",
"reason_code": "not_received",
"reason_description": "string",
"opened_block": null,
"opened_at": "2026-10-01T12:00:00Z",
"closed_by": "payer",
"close_reason": null,
"close_reason_code": "resolved_with_merchant",
"close_reason_description": null,
"closed_block": null,
"closed_at": null
}
]