{"openapi":"3.0.3","info":{"title":"FastPay API","version":"1.0.0","description":"Combined machine-readable reference for both FastPay HTTP surfaces, merged 2026-09-07 from two previously separate specs (openapi/mass-disbursement.yaml, openapi/payment-gateway.yaml) into this single canonical file - the earlier split risked exactly the drift a two-file setup invites, and this repo's own docs-sync discipline (root CLAUDE.md) treats one source of truth as the correct fix rather than a documentation nicety.\n\nThese are two independently deployed processes (see root CLAUDE.md's \"Three products, three trust models\" framing), sharing this one document for discovery convenience:\n\n- **Disbursement Engine** (`services/server.js`) - a single custodial engine. FastPay's own\n  wallet pays out to caller-supplied addresses; admin-key-authenticated, server-to-server.\n  See /docs/mass-disbursement-api.md for the full narrative reference.\n\n- **Relay Gateway** (`relay/server.js`) - multi-tenant, non-custodial checkout and\n  per-tenant mass payout. Per-tenant API-key-authenticated. See /docs/payment-gateway-api.md\n  for the full narrative reference.\n\n\n`servers` below lists both base URLs; every operation is tagged `Disbursement Engine` or `Relay Gateway` (or both) so a reader - or a tool like Redoc, which groups its sidebar by tag - can tell which process to call. Two relative paths, `/health` and `/admin/stats`, exist identically on BOTH processes with slightly different response shapes; each is documented once below with a combined schema covering both variants rather than being forced to pick one - see each operation's own description.\n\nEvery non-2xx Relay Gateway response is a JSON body matching the ErrorEnvelope schema below - see /docs/errors.md for the full reference (which fields are stable to branch on, per-route error_type/error_slug tables, and the history of this API's two now-converged error shapes). The Disbursement Engine's own error shape is DisbursementErrorEnvelope - genuinely different (a numeric error_code, no request_id) and kept as its own schema rather than folded into the Relay Gateway's, since the two engines were never meant to share a wire-level error contract.\n"},"servers":[{"url":"http://localhost:3000","description":"Disbursement Engine (Mass Disbursement API)"},{"url":"http://localhost:4001","description":"Relay Gateway (Payment Gateway API)"}],"security":[{"BearerApiKey":[]}],"tags":[{"name":"Disbursement Engine","description":"Single custodial engine - FastPay's own wallet pays out, webhook-driven."},{"name":"Relay Gateway","description":"Multi-tenant, non-custodial checkout + mass payout."}],"paths":{"/health":{"get":{"summary":"Liveness check","description":"Identical relative path on both separately-deployed processes (Disbursement Engine, default port 3000; Relay Gateway, default port 4001 - see `servers` above). The Disbursement Engine's own response additionally reports broadcast-ledger reachability and pause state (DisbursementHealth below); the Relay Gateway's is a simpler up/down check with no fixed schema.\n","tags":["Disbursement Engine","Relay Gateway"],"security":[],"responses":{"200":{"description":"Healthy","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/DisbursementHealth"},{"type":"object","description":"Relay Gateway's own shape - a bare liveness signal, no ledger/pause detail."}]}}}},"503":{"description":"Broadcast ledger unreachable (Disbursement Engine) or database unreachable (Relay Gateway)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisbursementHealth"}}}}}}},"/disburse":{"post":{"summary":"Trigger a disbursement job","description":"Validates the request synchronously and returns 202 immediately; processing and webhook delivery happen asynchronously. See /docs/webhooks.md for the per-payment status callbacks this job generates.\n","tags":["Disbursement Engine"],"security":[{"AdminApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisburseRequest"}}}},"responses":{"200":{"description":"Every payment in the request was a duplicate and was skipped","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisburseAccepted"}}}},"202":{"description":"Job accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisburseAccepted"}}}},"400":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisbursementErrorEnvelope"}}}},"403":{"description":"Missing or invalid x-admin-api-key"},"503":{"description":"System paused by an operator (POST /admin/pause)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"pausedAt":{"type":"string","format":"date-time"},"pausedReason":{"type":"string","nullable":true}}}}}}}}},"/status/{jobId}":{"get":{"summary":"Best-effort job status snapshot","description":"Not a substitute for webhooks. Falls back to checking for a CSV report file once the job has left memory (e.g. after a restart) - see the \"What's NOT authoritative: CSV reports\" section of the root CLAUDE.md.\n","tags":["Disbursement Engine"],"security":[],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Job status","content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["running"]},"progress":{"type":"object","properties":{"total":{"type":"integer"},"completed":{"type":"integer"},"failed":{"type":"integer"},"pending":{"type":"integer"}}}}},{"type":"object","properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["completed"]},"message":{"type":"string"}}}]}}}},"400":{"description":"jobId is not a valid UUID"},"404":{"description":"Job not found (not in memory, no report file either)"}}}},"/payments":{"get":{"summary":"List every allocation this server has ever accepted (paginated)","description":"Added 2026-09-05. Sourced from utils/deduplication.js's own record of every accepted allocation - a pre-flight rejection is listed here even though it never reaches utils/broadcastLedger.js (which only gets a row once a broadcast attempt is actually made). Each entry uses the same status values GET /payments/{allocationId} returns.\n","tags":["Disbursement Engine"],"security":[{"AdminApiKey":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"starting_after","in":"query","required":false,"schema":{"type":"string"},"description":"A previous page's last item's allocation_id."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["processing","processed"]},"description":"Filter by the underlying deduplication state."}],"responses":{"200":{"description":"Allocations","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"type":"object","properties":{"allocation_id":{"type":"string"},"status":{"type":"string","enum":["in_process","completed","failed","unknown"]},"tx_hash":{"type":"string","nullable":true},"chain":{"type":"string","nullable":true},"wallet":{"type":"string","nullable":true},"amount":{"type":"string","nullable":true},"currency":{"type":"string","nullable":true}}}},"has_more":{"type":"boolean"}}}}}},"400":{"description":"Invalid limit, starting_after, or status"},"403":{"description":"Missing or invalid x-admin-api-key"}}}},"/payments/{allocationId}":{"get":{"summary":"Retrieve one payment's current state by ALLOCATION_ID","description":"Added 2026-09-04. Reads directly from the durably-persisted deduplication store and broadcast ledger - see root CLAUDE.md's \"What IS authoritative\" section - rather than a separate Payment object this API doesn't otherwise track. Requires x-admin-api-key (unlike GET /status/{jobId}'s coarser, unauthenticated progress counts) since this returns real per-payment financial detail.\n","tags":["Disbursement Engine"],"security":[{"AdminApiKey":[]}],"parameters":[{"name":"allocationId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Payment status","content":{"application/json":{"schema":{"type":"object","properties":{"allocation_id":{"type":"string"},"status":{"type":"string","enum":["in_process","completed","failed","unknown"]},"tx_hash":{"type":"string","nullable":true},"chain":{"type":"string","nullable":true},"wallet":{"type":"string","nullable":true},"amount":{"type":"string","nullable":true},"currency":{"type":"string","nullable":true},"protocol_fee":{"type":"string","nullable":true},"treasury":{"type":"string","nullable":true},"attempts":{"type":"array","items":{"type":"object","properties":{"tx_hash":{"type":"string","nullable":true},"status":{"type":"string","enum":["SIGNED","SUBMITTED","SUBMIT_ERRORED","CONFIRMED","FAILED_ONCHAIN","SUPERSEDED"]},"nonce":{"type":"integer","nullable":true},"broadcast_started_at":{"type":"string","format":"date-time"},"broadcast_resolved_at":{"type":"string","format":"date-time","nullable":true},"submit_error":{"type":"string","nullable":true}}}}}}}}},"400":{"description":"Invalid allocationId (too long, or contains non-printable-ASCII characters)"},"403":{"description":"Missing or invalid x-admin-api-key"},"404":{"description":"This ALLOCATION_ID has never been accepted by this server"}}}},"/cancel":{"post":{"summary":"Abort an in-flight job","description":"In-memory only - does not survive a restart or un-broadcast an already-sent transaction.","tags":["Disbursement Engine"],"security":[{"AdminApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["job_id"],"properties":{"job_id":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Cancellation initiated"},"400":{"description":"Missing job_id"},"403":{"description":"Missing or invalid x-admin-api-key"},"404":{"description":"Job not found or already completed"}}}},"/admin/pause":{"post":{"summary":"System-wide kill switch - stop accepting new payments","tags":["Disbursement Engine"],"security":[{"AdminApiKey":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Paused"},"403":{"description":"Missing or invalid x-admin-api-key"}}}},"/admin/resume":{"post":{"summary":"Reverse /admin/pause","tags":["Disbursement Engine"],"security":[{"AdminApiKey":[]}],"responses":{"200":{"description":"Resumed"},"403":{"description":"Missing or invalid x-admin-api-key"}}}},"/admin/reset":{"post":{"summary":"Wipe all deduplication state (destructive)","tags":["Disbursement Engine"],"security":[{"AdminApiKey":[]}],"responses":{"200":{"description":"Reset complete"},"403":{"description":"Missing or invalid x-admin-api-key"},"500":{"description":"Reset failed"}}}},"/admin/remove-processed-ids":{"post":{"summary":"Re-admit specific allocation IDs without wiping everything","tags":["Disbursement Engine"],"security":[{"AdminApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["allocation_ids"],"properties":{"allocation_ids":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Removal result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"removed":{"type":"array","items":{"type":"string"}},"notFound":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"Missing or empty allocation_ids"},"403":{"description":"Missing or invalid x-admin-api-key"}}}},"/admin/stats":{"get":{"summary":"RPC/bundler/connector provider usage snapshot for this process","description":"Same relative path on both processes, each reporting its OWN process's provider usage (see services/connectors/usageSnapshot.js for the Disbursement Engine; the Relay Gateway's own connector registry - see root CLAUDE.md's \"Provider/connector layer\" section - for the other). Response shape below reflects the Disbursement Engine's own fields (success/deduplication/connectors/timestamp); the Relay Gateway's own response carries the same {vendor, tier, usage, budget} connectors array via a separate registry instance, without a `deduplication` field (that concept is Disbursement-Engine-only).\n","tags":["Disbursement Engine","Relay Gateway"],"security":[{"AdminApiKey":[]}],"responses":{"200":{"description":"Stats","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"deduplication":{"type":"object","description":"Disbursement Engine only."},"connectors":{"type":"array","items":{"type":"object"}},"timestamp":{"type":"string","format":"date-time"}}}}}},"401":{"description":"Missing or invalid x-admin-api-key (Relay Gateway)"},"403":{"description":"Missing or invalid x-admin-api-key (Disbursement Engine)"}}}},"/v1/networks":{"get":{"summary":"List every network this API accepts, and whether checkout can actually fulfill payments on it yet","description":"Added 2026-09-07, closing a real gap found comparing this API's documentation against a mature crypto payment API's public reference: nothing exposed which networks/currencies are supported programmatically before this. Public, unauthenticated, static reference data - relay/catalog.js.\n","tags":["Relay Gateway"],"security":[],"responses":{"200":{"description":"Networks","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"type":"object","properties":{"object":{"type":"string","enum":["network"]},"network":{"type":"string"},"live":{"type":"boolean","description":"Whether checkout can actually fulfill a payment on this network today - see LIVE_NETWORKS in relay/paymentRequests/fulfillment.js. A network can be accepted at payment-request creation (this list) without yet being live."}}}}}}}}}}}},"/v1/currencies":{"get":{"summary":"List every currency/network pair this API accepts","description":"Added 2026-09-07. Public, unauthenticated, static reference data - relay/catalog.js.","tags":["Relay Gateway"],"security":[],"responses":{"200":{"description":"Currencies","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"type":"object","properties":{"object":{"type":"string","enum":["currency"]},"currency":{"type":"string"},"network":{"type":"string"},"decimals":{"type":"integer"},"type":{"type":"string","enum":["native","token"]},"live":{"type":"boolean"}}}}}}}}}}}},"/v1/rates":{"get":{"summary":"Look up the current fiat<->crypto exchange rate for a pair","description":"Added 2026-09-07. Wraps the same relay/paymentRequests/rateProvider.js#getExchangeRate the Checkout Sessions quoting engine uses internally (POST /v1/checkout/sessions) - not a second rate source. This is NOT a locked quote; only creating a checkout session locks one. Public, unauthenticated.\n","tags":["Relay Gateway"],"security":[],"parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string"},"description":"Fiat or crypto currency code, e.g. EUR or USDC."},{"name":"to","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Rate","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["rate"]},"from":{"type":"string"},"to":{"type":"string"},"rate":{"type":"string"},"source":{"type":"string","enum":["live","live_stale","static_fallback","identity"]},"fetched_at":{"type":"string","format":"date-time","nullable":true}}}}}},"400":{"description":"from or to missing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"No rate available for this pair (live fetch failed, no static fallback configured)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/instructions":{"post":{"summary":"Submit a mass-payout batch for the calling tenant's own smart account","tags":["Relay Gateway"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["network","payments"],"properties":{"network":{"type":"string","enum":["ethereum","tron","solana"]},"payments":{"type":"array","items":{"$ref":"#/components/schemas/DisbursementPayment"}},"callbackUrl":{"type":"string","format":"uri"}}}}}},"responses":{"202":{"description":"Job accepted (or awaiting onboarding)","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"type":"string","enum":["accepted","awaiting_onboarding"]}}}}}},"400":{"description":"Validation error (Shape A envelope)"},"402":{"description":"Tenant daily payment cap exceeded"},"503":{"description":"System paused"}}}},"/v1/jobs/{jobId}":{"get":{"summary":"Retrieve a mass-payout job's status","tags":["Relay Gateway"],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job"},"404":{"description":"Job not found"}}}},"/v1/jobs/{jobId}/cancel":{"post":{"summary":"Cancel a job (only while accepted/awaiting_onboarding)","tags":["Relay Gateway"],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancelled"},"404":{"description":"Job not found"},"409":{"description":"Job is no longer cancellable"}}}},"/v1/payouts/{id}":{"get":{"summary":"Retrieve a single mass-payout entry as its own resource (added 2026-09-03)","description":"Addressed by your own ALLOCATION_ID, not the job id. A thin, real view over the same job_payments row GET /v1/jobs/{jobId}'s own payments[] array already includes - not a second money-moving system (mass payout is already non-custodial).","tags":["Relay Gateway"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Payout","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payout"}}}},"404":{"description":"No such payout for this tenant"}}}},"/v1/jobs/{jobId}/payouts":{"get":{"summary":"List every payout entry for one job (added 2026-09-03)","tags":["Relay Gateway"],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Payouts","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Payout"}}}}}}},"404":{"description":"Job not found"}}}},"/v1/payment-requests":{"post":{"summary":"Create a checkout payment request (mature path, fires a `paid` webhook)","tags":["Relay Gateway"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRequestCreate"}}}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":200}}],"responses":{"200":{"description":"Idempotent replay of a prior creation"},"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRequest"}}}},"400":{"description":"Validation error (Shape A envelope)"},"409":{"description":"Idempotency key reused with a different body"}}}},"/v1/payment-requests/{requestId}":{"get":{"summary":"Retrieve a payment request (tenant-authenticated, full object)","tags":["Relay Gateway"],"parameters":[{"name":"requestId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Payment request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRequest"}}}},"404":{"description":"Not found"}}}},"/v1/payment-requests/{requestId}/public":{"get":{"summary":"Retrieve the public (unauthenticated) view of a payment request","tags":["Relay Gateway"],"security":[],"parameters":[{"name":"requestId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Public view","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRequestPublic"}}}},"404":{"description":"Not found"},"429":{"description":"Rate limited"}}}},"/v1/payment-requests/{requestId}/intent":{"post":{"summary":"Build the unsigned transaction(s) for a connected wallet to sign","tags":["Relay Gateway"],"security":[],"parameters":[{"name":"requestId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["payerAddress"],"properties":{"payerAddress":{"type":"string"}}}}}},"responses":{"200":{"description":"Unsigned transaction intent (Ethereum or Tron shaped)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentIntent"}}}},"400":{"description":"Invalid payerAddress","or network not yet live":null},"404":{"description":"Not found"},"409":{"description":"Already paid or expired"},"429":{"description":"Rate limited"}}}},"/v1/payment-requests/{requestId}/confirm":{"post":{"summary":"Confirm a broadcast transaction hash against on-chain state","tags":["Relay Gateway"],"security":[],"parameters":[{"name":"requestId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["txHash"],"properties":{"txHash":{"type":"string","description":"32-byte hex, 0x-prefixed for Ethereum, unprefixed for Tron."}}}}}},"responses":{"200":{"description":"Confirmation result","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["paid","pending","failed"]},"txHash":{"type":"string"},"reason":{"type":"string","description":"Added 2026-09-03. Present only when status is \"failed\" - names why (tx_reverted/wrong_contract/no_disbursed_event/amount_mismatch/recipient_mismatch/token_mismatch). A classic underpayment/overpayment cannot reach amount_mismatch through the normal flow - the disburser contract enforces the exact quoted amount, so a wrong amount reverts outright (tx_reverted) rather than landing here.","enum":["tx_reverted","wrong_contract","no_disbursed_event","amount_mismatch","recipient_mismatch","token_mismatch","sandbox_forced_failure"]}}}}}},"400":{"description":"Invalid txHash","or network not yet live":null},"404":{"description":"Not found"},"409":{"description":"Already finalized"},"429":{"description":"Rate limited"}}}},"/v1/checkout/sessions":{"post":{"summary":"Create a fiat-quoted checkout session (newer path; fires a paid webhook if callback_url is set)","tags":["Relay Gateway"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutSessionCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutSession"}}}},"400":{"description":"Validation error (Shape B envelope)"}}}},"/v1/checkout/sessions/{id}":{"get":{"summary":"Retrieve a checkout session, including its payments","tags":["Relay Gateway"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Checkout session"},"404":{"description":"Not found"}}}},"/v1/checkout/sessions/{id}/expire":{"post":{"summary":"Manually expire a checkout session","tags":["Relay Gateway"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Expired"},"404":{"description":"Not found"}}}},"/v1/payments/{id}":{"get":{"summary":"Retrieve full status history, financials, and blockchain data for a payment","tags":["Relay Gateway"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Payment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payment"}}}},"404":{"description":"Not found"}}}},"/v1/payments/{id}/transactions":{"get":{"summary":"Every payment attempt's on-chain data (added 2026-09-03)","description":"Not just the most recent attempt Payment.blockchain already summarizes - a payer can retry with a different currency/network after an earlier attempt expired or was abandoned.","tags":["Relay Gateway"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Payment attempts","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/BlockchainTransaction"}}}}}}},"404":{"description":"No such payment for this tenant"}}}},"/v1/payments/{id}/refunds":{"post":{"summary":"Create a refund - checkout_session payments only, Ethereum only","description":"Non-custodial like the payment itself: returns an unsigned transaction for the merchant's own wallet to sign and broadcast (FastPay never holds the funds to refund them directly). Destination defaults to the original payer's wallet.\n","tags":["Relay Gateway"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount"],"properties":{"amount":{"type":"string","description":"Positive decimal string, in the payment's own paymentCurrency"},"reason":{"type":"string"},"destination":{"type":"string","description":"Optional override - defaults to the original payer's address"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Refund"}}}},"400":{"description":"Validation error, e.g. amount_exceeds_refundable, unsupported_network"},"404":{"description":"No such payment"},"409":{"description":"Payment is not in \"paid\" status"}}}},"/v1/refunds/{id}":{"get":{"summary":"Retrieve a refund","tags":["Relay Gateway"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Refund","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Refund"}}}},"404":{"description":"Not found"}}}},"/v1/refunds/{id}/confirm":{"post":{"summary":"Confirm a broadcast refund transaction against on-chain state","description":"Merchant-authenticated (unlike a payment's own confirm route) - only the merchant who requested and signed the refund should report it. Never trusts the hash alone.\n","tags":["Relay Gateway"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["txHash"],"properties":{"txHash":{"type":"string"}}}}}},"responses":{"200":{"description":"Confirmation result","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","enum":["refund"]},"status":{"type":"string","enum":["pending","confirmed","failed"]},"tx_hash":{"type":"string","nullable":true}}}}}},"400":{"description":"Invalid txHash"},"404":{"description":"Not found"}}}},"/v1/balance":{"get":{"summary":"List balances across all currencies for the calling tenant","tags":["Relay Gateway"],"responses":{"200":{"description":"Balances","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Balance"}}}}}}}}}},"/v1/balance/{currency}":{"get":{"summary":"Retrieve balance for a single currency","tags":["Relay Gateway"],"parameters":[{"name":"currency","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"}}}}}}},"/v1/balance/transactions":{"get":{"summary":"List every ledger entry for the calling tenant (paginated; format=csv is a full unpaginated export)","tags":["Relay Gateway"],"parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["csv"]},"description":"Added 2026-09-03. Omit for the default paginated JSON response; \"csv\" returns the tenant's ENTIRE history (unaffected by limit/starting_after) as a downloadable text/csv file instead."},{"$ref":"#/components/parameters/ListLimit"},{"$ref":"#/components/parameters/ListStartingAfter"}],"responses":{"200":{"description":"Ledger entries (paginated JSON by default, or the full CSV file if format=csv)","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/LedgerTransaction"}},"has_more":{"type":"boolean","description":"Added 2026-09-04. Absent from the format=csv response (that one is never paginated)."}}}},"text/csv":{"schema":{"type":"string"}}}},"400":{"description":"limit out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/reconciliation":{"get":{"summary":"Cross-reference business/on-chain/ledger state for paid payments and report discrepancies","description":"Added 2026-09-03. Read-only. Cross-references payments.status='paid', payment_attempts (kept current for network=ethereum by relay/confirmationSweep.js, reorgs included), and the double-entry ledger's own payable credit - none of which check themselves against the others anywhere else in this API.\n","tags":["Relay Gateway"],"parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"ISO-8601 lower bound on when each payment reached 'paid'. Defaults to 30 days before 'to'."},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"ISO-8601 upper bound on when each payment reached 'paid'. Defaults to now."}],"responses":{"200":{"description":"Reconciliation report","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReconciliationReport"}}}},"400":{"description":"Malformed from/to","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/webhook-deliveries":{"get":{"summary":"List this tenant's own webhook delivery attempts (paginated)","description":"Added 2026-09-04; response_status/response_body added 2026-09-07. Closes the \"no webhook delivery dashboard visible to a merchant\" gap - exposes relay/webhooks/outbox.js's own webhook_deliveries table, previously internal only. See /docs/webhooks.md's own \"Delivery log\" section for exactly what's included, and POST /v1/webhook-deliveries/{id}/redeliver below for on-demand redelivery.\n","tags":["Relay Gateway"],"parameters":[{"$ref":"#/components/parameters/ListLimit"},{"$ref":"#/components/parameters/ListStartingAfter"},{"name":"allocation_id","in":"query","required":false,"schema":{"type":"string"},"description":"Filter to one payment/allocation's own delivery attempts."}],"responses":{"200":{"description":"Webhook deliveries","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"has_more":{"type":"boolean"}}}}}},"400":{"description":"limit out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/webhook-deliveries/{id}":{"get":{"summary":"Retrieve one webhook delivery, including the full payload sent","tags":["Relay Gateway"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook delivery","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookDelivery"},{"type":"object","properties":{"payload":{"type":"object","description":"The exact JSON body sent (or queued to be sent) for this event."}}}]}}}},"404":{"description":"No such delivery for this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/webhook-deliveries/{id}/redeliver":{"post":{"summary":"Redeliver one webhook delivery on demand","description":"Added 2026-09-07. Runs the exact same send-and-record logic the periodic redelivery sweep uses, immediately rather than waiting for the sweep. Claimable even for an already-delivered row (re-sending one your receiver already acknowledged). Returns 409 if a redelivery (this endpoint, or the periodic sweep) is already in flight for the row.\n","tags":["Relay Gateway"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The delivery's updated state after the redelivery attempt completed","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookDelivery"},{"type":"object","properties":{"payload":{"type":"object","description":"The exact JSON body sent for this event."}}}]}}}},"404":{"description":"No such delivery for this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"A redelivery is already in flight for this row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/customers":{"post":{"summary":"Create a customer","tags":["Relay Gateway"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"name":{"type":"string"},"country":{"type":"string"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"400":{"description":"Validation error (Shape B envelope)"}}},"get":{"summary":"List customers for the calling tenant (paginated)","tags":["Relay Gateway"],"parameters":[{"$ref":"#/components/parameters/ListLimit"},{"$ref":"#/components/parameters/ListStartingAfter"}],"responses":{"200":{"description":"Customers","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Customer"}},"has_more":{"type":"boolean","description":"Added 2026-09-04."}}}}}},"400":{"description":"limit out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/customers/{id}":{"get":{"summary":"Retrieve a customer","tags":["Relay Gateway"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Customer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"404":{"description":"Not found"}}}},"/v1/payment-channels":{"post":{"summary":"Create a reusable deposit-address payment channel for a customer","tags":["Relay Gateway"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer_id","networks"],"properties":{"customer_id":{"type":"string"},"networks":{"type":"array","items":{"type":"string","enum":["ethereum","tron","solana"]}}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentChannel"}}}},"400":{"description":"Validation error (Shape B envelope)"},"404":{"description":"Customer not found"}}},"get":{"summary":"List payment channels for the calling tenant (paginated)","tags":["Relay Gateway"],"parameters":[{"$ref":"#/components/parameters/ListLimit"},{"$ref":"#/components/parameters/ListStartingAfter"}],"responses":{"200":{"description":"Payment channels","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/PaymentChannel"}},"has_more":{"type":"boolean","description":"Added 2026-09-04."}}}}}},"400":{"description":"limit out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/payment-channels/{id}":{"get":{"summary":"Retrieve a payment channel","tags":["Relay Gateway"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Payment channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentChannel"}}}},"404":{"description":"Not found"}}}},"/v1/payment-channels/{id}/close":{"post":{"summary":"Close a payment channel","tags":["Relay Gateway"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Closed"},"404":{"description":"Not found"}}}},"/admin/audit-log":{"get":{"summary":"Immutable \"who did what, when\" trail for admin/tenant writes on this relay","description":"Never contains raw secrets (API keys, private keys). tenant_id matches both actions the tenant itself performed and admin actions performed on it.","tags":["Relay Gateway"],"security":[{"AdminApiKey":[]}],"parameters":[{"name":"tenant_id","in":"query","schema":{"type":"string"}},{"name":"target_type","in":"query","schema":{"type":"string","example":"tenant"}},{"name":"target_id","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}}],"responses":{"200":{"description":"Audit events, newest first","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","example":"aud_..."},"object":{"type":"string","enum":["audit_event"]},"actor_type":{"type":"string","enum":["admin","tenant"]},"actor_id":{"type":"string","nullable":true,"description":"The tenant id for actor_type=tenant; always null for actor_type=admin (one shared admin key, no per-user identity)."},"action":{"type":"string","example":"tenant.limits_changed"},"target_type":{"type":"string","nullable":true},"target_id":{"type":"string","nullable":true},"details":{"type":"object","description":"Action-specific, e.g. { before, after } for status/limits changes."},"ip_address":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"description":"Missing or invalid x-admin-api-key"}}}},"/admin/tenants":{"post":{"summary":"Create a tenant (operator only)","tags":["Relay Gateway"],"security":[{"AdminApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"status":{"type":"string","enum":["active"]},"webhookSecret":{"type":"string","description":"whsec_-prefixed, shown once"}}}}}}}}},"/admin/tenants/{id}/wallets":{"post":{"summary":"Register a tenant payout wallet for a network (operator only)","tags":["Relay Gateway"],"security":[{"AdminApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["chain","address"],"properties":{"chain":{"type":"string","enum":["ethereum","solana","tron"]},"address":{"type":"string"}}}}}},"responses":{"200":{"description":"Registered"},"400":{"description":"Invalid chain/address"},"404":{"description":"Tenant not found"}}},"get":{"summary":"List a tenant's registered wallets (operator only)","tags":["Relay Gateway"],"security":[{"AdminApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Wallets"},"404":{"description":"Tenant not found"}}}},"/admin/tenants/{id}/api-keys":{"post":{"summary":"Mint an API key for a tenant (operator only, shown once)","tags":["Relay Gateway"],"security":[{"AdminApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"test":{"type":"boolean","default":false}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"apiKey":{"type":"string"},"prefix":{"type":"string"},"warning":{"type":"string"}}}}}},"404":{"description":"Tenant not found"}}}},"/admin/tenants/{id}/status":{"post":{"summary":"Suspend or reactivate a tenant (operator only)","tags":["Relay Gateway"],"security":[{"AdminApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated"},"404":{"description":"Tenant not found"}}}}},"components":{"securitySchemes":{"BearerApiKey":{"type":"http","scheme":"bearer","description":"Per-tenant API key, e.g. \"Authorization: Bearer fp_live_...\" (Relay Gateway)."},"AdminApiKey":{"type":"apiKey","in":"header","name":"x-admin-api-key","description":"Shared admin key (Disbursement Engine: ADMIN_API_KEY; Relay Gateway: RELAY_ADMIN_API_KEY) - same header name, two independently configured values per process."}},"parameters":{"ListLimit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Added 2026-09-04. Max items to return, 1-100 (default 20)."},"ListStartingAfter":{"name":"starting_after","in":"query","required":false,"schema":{"type":"string"},"description":"Added 2026-09-04. A previous page's last item id - returns the next page after it, most-recently-created first."}},"schemas":{"DisbursementHealth":{"type":"object","description":"Disbursement Engine's own /health response shape.","properties":{"status":{"type":"string","enum":["ok","error"]},"ledger":{"type":"string","enum":["ok","unreachable"]},"paused":{"type":"boolean"}}},"DisbursementPayment":{"type":"object","description":"One payment inside a Disbursement Engine batch (POST /disburse) or a Relay mass-payout batch (POST /v1/instructions) - the same shape serves both.","required":["ALLOCATION_ID","CHAIN","WALLET","CURRENCY","AMOUNT"],"properties":{"ALLOCATION_ID":{"type":"string","maxLength":256,"description":"Caller-supplied idempotency key for this payment."},"CHAIN":{"type":"string","description":"One of utils/chainRegistry.js's VALID_CHAIN_KEYS. A valid value does not guarantee a live, configured adapter in this deployment - an unconfigured chain fails cleanly per-payment via a `failed` webhook.\n","example":"ETH"},"WALLET":{"type":"string","description":"Recipient address, validated per-chain downstream."},"CURRENCY":{"type":"string","example":"USDC"},"AMOUNT":{"oneOf":[{"type":"number"},{"type":"string"}],"description":"Must be > 0 and <= MAX_AMOUNT_PER_PAYMENT (default 1,000,000)."}}},"DisburseRequest":{"type":"object","required":["payments","callbackUrl"],"properties":{"payments":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/DisbursementPayment"}},"callbackUrl":{"type":"string","format":"uri","description":"Public http(s) URL - loopback/private/link-local targets are rejected."},"resetDeduplication":{"type":"boolean","default":false,"description":"Clears ALL deduplication state before processing this batch. Use with care."}}},"DisburseAccepted":{"type":"object","properties":{"jobId":{"type":"string","format":"uuid"},"message":{"type":"string"},"payments_accepted":{"type":"integer"},"duplicates_skipped":{"type":"integer"},"skipped_allocations":{"type":"array","items":{"type":"string"}}}},"DisbursementErrorEnvelope":{"type":"object","description":"The Disbursement Engine's own error shape - genuinely different from the Relay Gateway's ErrorEnvelope below (a numeric error_code, no request_id) - kept separate rather than folded together.","properties":{"error_code":{"type":"integer"},"error_type":{"type":"string"},"error_slug":{"type":"string"},"error":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer"},"code":{"type":"integer"},"type":{"type":"string"},"slug":{"type":"string"},"message":{"type":"string"}}}}}},"ErrorEnvelope":{"type":"object","description":"The Relay Gateway's own error shape for every non-2xx response. Converged 2026-09-03 (relay/errors/envelope.js) - see /docs/errors.md for the full history. error_type/ error_slug/error_message are always present and identically named regardless of which route produced the error; branch new code on those three. The legacy `error` field is also always present, kept only for backward compatibility - it is a plain string on older routes (/v1/instructions, /v1/payment-requests, /v1/jobs/*, auth, quota) and a nested {type, code, message} object on newer ones (checkout sessions, /v1/payments/*, refunds, customers, payment channels) - do not rely on its type being consistent across routes.\n","properties":{"error_type":{"type":"string","nullable":true,"description":"Stable category, e.g. request_error, authentication_error, quota_error, api_error."},"error_slug":{"type":"string","nullable":true,"description":"Stable, specific machine-readable reason within error_type."},"error_message":{"type":"string","nullable":true,"description":"Human-readable description. Free text - do not branch program logic on its wording."},"request_id":{"type":"string","nullable":true,"description":"Added 2026-09-05. Matches this same response's own X-Request-ID header - use to correlate a specific response with a server-side log line."},"error":{"description":"Legacy field, kept for backward compatibility only - see this schema's own top-level description.","oneOf":[{"type":"string"},{"type":"object","properties":{"type":{"type":"string"},"code":{"type":"string"},"message":{"type":"string"},"param":{"type":"string","nullable":true},"request_id":{"type":"string","nullable":true}}}]},"details":{"type":"array","nullable":true,"description":"Present on some request-validation failures (e.g. multi-field payment validation) - one entry per invalid field.","items":{"type":"object"}}}},"WebhookDelivery":{"type":"object","description":"Added 2026-09-04 (relay/webhooks/outbox.js). One attempted (or still-pending) webhook delivery.","properties":{"id":{"type":"string"},"object":{"type":"string","enum":["webhook_delivery"]},"event_id":{"type":"string","description":"Matches the payload's own event_id field - safe to dedupe on across retries of the same occurrence."},"allocation_id":{"type":"string"},"job_id":{"type":"string","nullable":true},"action":{"type":"string"},"status":{"type":"string"},"callback_url":{"type":"string"},"attempts":{"type":"integer"},"last_error":{"type":"string","nullable":true,"description":"Free-text failure summary."},"response_status":{"type":"integer","nullable":true,"description":"The receiver's HTTP status code from the most recent delivery attempt. Null if no HTTP response was ever received (a network-level failure - timeout, DNS, connection refused)."},"response_body":{"type":"string","nullable":true,"description":"The receiver's response body from the most recent delivery attempt, truncated to 4096 characters. Null under the same conditions as response_status."},"delivered":{"type":"boolean"},"delivered_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ReconciliationReport":{"type":"object","description":"Added 2026-09-03 (relay/reconciliation/report.js). Cross-references business/on-chain/ledger state for every payment that reached 'paid' within the requested period.","properties":{"object":{"type":"string","enum":["reconciliation_report"]},"tenant_id":{"type":"string"},"period":{"type":"object","properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"}}},"summary":{"type":"object","properties":{"payments_paid":{"type":"integer"},"total_settlement_amount_by_currency":{"type":"object","additionalProperties":{"type":"string"},"description":"Keyed by payments.settlement_currency (the fiat side for a checkout_session payment)."},"payments_with_discrepancy":{"type":"integer"}}},"discrepancies":{"type":"array","items":{"type":"object","properties":{"payment_id":{"type":"string"},"type":{"type":"string","enum":["chain_unconfirmed","chain_reorged","ledger_missing","ledger_amount_mismatch"]},"detail":{"type":"string"}}}}}},"Payout":{"type":"object","description":"Added 2026-09-03. Deliberately carries no fee/conversion breakdown - that fee is paid on-chain by the tenant's own smart account directly to FastPayDisburser, not tracked in this off-chain row.","properties":{"id":{"type":"string","description":"The tenant's own ALLOCATION_ID from POST /v1/instructions."},"object":{"type":"string","enum":["payout"]},"job_id":{"type":"string"},"network":{"type":"string","enum":["ethereum","tron","solana"]},"destination":{"type":"string"},"currency":{"type":"string"},"amount":{"type":"string"},"status":{"type":"string"},"tx_hash":{"type":"string","nullable":true},"error_message":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"BlockchainTransaction":{"type":"object","description":"Added 2026-09-03. One payment attempt - a payer can retry with a different currency/network, so a payment may have more than one of these.","properties":{"id":{"type":"string"},"object":{"type":"string","enum":["blockchain_transaction"]},"payment_id":{"type":"string"},"network":{"type":"string"},"currency":{"type":"string"},"amount":{"type":"string"},"address":{"type":"string"},"status":{"type":"string"},"tx_hash":{"type":"string","nullable":true},"confirmations":{"type":"integer","nullable":true,"description":"Real, live-updated confirmation count for network=ethereum (relay/confirmationSweep.js, added 2026-09-03, polls at RELAY_CONFIRMATION_SWEEP_INTERVAL_MS). Still always null for every other network - no confirmation engine exists for them yet."},"required_confirmations":{"type":"integer","nullable":true,"description":"The threshold confirmations must reach before this transaction is considered final (RELAY_REQUIRED_CONFIRMATIONS_ETHEREUM, default 12, for network=ethereum). Null for a network with no confirmation engine."},"detected_at":{"type":"string","format":"date-time","nullable":true},"confirmed_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"Payment":{"type":"object","description":"Relay Gateway's own Payment resource (GET /v1/payments/{id}) - distinct from DisbursementPayment above, which is a mass-payout batch entry, not a retrievable resource.","properties":{"object":{"type":"string","enum":["payment"]},"id":{"type":"string"},"status":{"type":"string"},"lifecycle":{"type":"array","items":{"type":"object","properties":{"status":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}}},"financial":{"type":"object","properties":{"originalAmount":{"type":"string"},"originalCurrency":{"type":"string"},"paymentAmount":{"type":"string"},"paymentCurrency":{"type":"string"},"exchangeRate":{"type":"string"},"gatewayFee":{"type":"string","nullable":true},"networkFee":{"type":"string","nullable":true},"conversionFee":{"type":"string","nullable":true},"netSettlement":{"type":"string","nullable":true},"settlementCurrency":{"type":"string","nullable":true},"settlementType":{"type":"string","enum":["ledger_credit"],"description":"The only settlement model this API implements today (utils/settlementTypes.js): crypto moves directly payer -> merchant's own wallet, non-custodial, and this is a bookkeeping record of that fact - not a real balance FastPay holds or could withdraw on your behalf. Always 'ledger_credit'."},"refundedAmount":{"type":"string","description":"Sum of confirmed + pending refunds, in paymentCurrency."},"refundableAmount":{"type":"string","description":"paymentAmount minus refundedAmount."}}},"blockchain":{"type":"object","nullable":true,"properties":{"network":{"type":"string"},"asset":{"type":"string"},"address":{"type":"string"},"txHash":{"type":"string","nullable":true},"confirmations":{"type":"integer","nullable":true,"description":"Real, live-updated confirmation count for network=ethereum (relay/confirmationSweep.js, added 2026-09-03). Still always null for every other network - no confirmation engine exists for them yet. See GET /v1/payments/:id/transactions for the per-attempt breakdown plus each attempt's required_confirmations threshold."}}},"related":{"type":"object","properties":{"checkoutSessionId":{"type":"string"},"customerId":{"type":"string","nullable":true},"refundIds":{"type":"array","items":{"type":"string"}},"ledgerTransactionIds":{"type":"array","items":{"type":"string"}}}}}},"Refund":{"type":"object","properties":{"id":{"type":"string","example":"re_..."},"object":{"type":"string","enum":["refund"]},"payment_id":{"type":"string"},"status":{"type":"string","enum":["pending","confirmed","failed"]},"amount":{"type":"string"},"currency":{"type":"string"},"destination":{"type":"string"},"reason":{"type":"string","nullable":true},"transaction":{"type":"object","description":"Present only on creation - the unsigned transaction for the merchant's own wallet to sign.","properties":{"chainId":{"type":"integer"},"from":{"type":"string"},"to":{"type":"string"},"data":{"type":"string"},"value":{"type":"string","nullable":true}}},"created_at":{"type":"string","format":"date-time"},"is_test":{"type":"boolean","description":"Inherited from the underlying payment's own is_test - see the Sandbox / test mode section in payment-gateway-api.md."}}},"PaymentRequestCreate":{"type":"object","required":["reference","network","currency","amount","expiresAt"],"properties":{"reference":{"type":"string","maxLength":200},"network":{"type":"string","enum":["ethereum","solana","tron"]},"currency":{"type":"string","description":"ETH/USDC/USDT for ethereum; SOL/USDC/USDT for solana; TRX/USDT for tron"},"amount":{"type":"string","description":"Positive decimal string"},"recipient":{"type":"string","description":"Optional if a wallet is pre-registered for this network via /admin/tenants/:id/wallets"},"expiresAt":{"type":"string","format":"date-time","description":"Future, at most 7 days out"},"callbackUrl":{"type":"string","format":"uri"},"metadata":{"type":"object"}}},"PaymentRequest":{"allOf":[{"$ref":"#/components/schemas/PaymentRequestCreate"},{"type":"object","properties":{"id":{"type":"string","example":"pr_..."},"status":{"type":"string","enum":["pending","paid","expired"]},"tenantId":{"type":"string"},"txHash":{"type":"string","nullable":true},"isTest":{"type":"boolean","description":"true if created with an fp_test_-prefixed API key - see Sandbox / test mode."}}}]},"PaymentRequestPublic":{"type":"object","properties":{"id":{"type":"string"},"reference":{"type":"string"},"network":{"type":"string"},"currency":{"type":"string"},"amount":{"type":"string"},"recipient":{"type":"string"},"status":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"txHash":{"type":"string","nullable":true},"redirectUrl":{"type":"string","nullable":true},"isTest":{"type":"boolean"}}},"PaymentIntent":{"type":"object","description":"Shape varies by network - Ethereum returns raw calldata, Tron returns a full unsigned transaction object. For a sandbox (is_test) payment request, this is instead a simulated descriptor (simulated: true, sandboxTxHashes: { failed, pending }, ...) with no real chain call - see Sandbox / test mode.\n","properties":{"paymentRequestId":{"type":"string"},"chainId":{"type":"integer"},"to":{"type":"string"},"data":{"type":"string","nullable":true,"description":"Ethereum only"},"value":{"type":"string","nullable":true,"description":"Ethereum only, hex-encoded wei for native-currency payments"},"transaction":{"type":"object","nullable":true,"description":"Tron only"},"approval":{"type":"object","nullable":true,"description":"Present only when an ERC20/TRC20 allowance approval is needed first.","properties":{"to":{"type":"string"},"data":{"type":"string"}}},"currency":{"type":"string"},"amount":{"type":"string"},"fee":{"type":"string"},"total":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"simulated":{"type":"boolean","description":"Sandbox mode only - true when this intent was built by the simulated adapter, not a real chain."}}},"CheckoutSessionCreate":{"type":"object","required":["amount","currency","success_url","cancel_url","network","settlement_currency"],"properties":{"amount":{"type":"string","description":"Positive decimal string, fiat"},"currency":{"type":"string","enum":["EUR","USD"]},"success_url":{"type":"string","format":"uri"},"cancel_url":{"type":"string","format":"uri"},"network":{"type":"string","enum":["ethereum","solana","tron"]},"settlement_currency":{"type":"string","description":"Must be quotable for the network - in practice BTC/ETH/USDC/USDT, not SOL/TRX"},"merchant_reference":{"type":"string"},"callback_url":{"type":"string","format":"uri","description":"Optional - receives a paid webhook on completion (SSRF-validated, public http(s) only)."},"customer":{"type":"object","properties":{"id":{"type":"string"}}}}},"CheckoutSession":{"type":"object","properties":{"id":{"type":"string","example":"cs_..."},"object":{"type":"string","enum":["checkout_session"]},"status":{"type":"string"},"payment_status":{"type":"string"},"amount":{"type":"string"},"currency":{"type":"string"},"network":{"type":"string"},"payment_amount":{"type":"string"},"payment_currency":{"type":"string"},"exchange_rate":{"type":"string"},"payment_url":{"type":"string","format":"uri"},"expires_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time"},"is_test":{"type":"boolean"}}},"Balance":{"type":"object","properties":{"currency":{"type":"string"},"available":{"type":"string"},"pending":{"type":"string"},"total":{"type":"string"}}},"LedgerTransaction":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","enum":["ledger_transaction"]},"transaction_id":{"type":"string"},"direction":{"type":"string","enum":["credit","debit"]},"amount":{"type":"string"},"currency":{"type":"string"},"balance_before":{"type":"string"},"balance_after":{"type":"string"},"reference_type":{"type":"string"},"reference_id":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}},"Customer":{"type":"object","properties":{"id":{"type":"string","example":"cus_..."},"object":{"type":"string","enum":["customer"]},"email":{"type":"string","format":"email"},"name":{"type":"string","nullable":true},"country":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"PaymentChannel":{"type":"object","properties":{"id":{"type":"string","example":"pc_..."},"object":{"type":"string","enum":["payment_channel"]},"customer_id":{"type":"string"},"status":{"type":"string","enum":["active","closed"]},"addresses":{"type":"array","items":{"type":"object","properties":{"currency":{"type":"string"},"network":{"type":"string"},"address":{"type":"string"}}}},"created_at":{"type":"string","format":"date-time"}}}}}}