{
  "schema_version": "1.0",
  "generated_at": "2026-09-04T19:08:39.673Z",
  "title": "FastPay API Documentation",
  "description": "Complete machine-readable catalogue for FastPay's documented API surfaces, limits, chains, lifecycle rules, and deployment boundaries.",
  "canonical_url": "https://f-pay.com/docs",
  "openapi_url": "https://xwefglhexusimsmdgvtd.supabase.co/functions/v1/openapi-spec",
  "status": {
    "relay_environment": "staging-only",
    "production_relay_deployed": false,
    "beta_product_note": "Checkout Sessions, Payments, Refunds, Balance & Ledger, and Customers & Payment Channels are beta-quality surfaces. The ledger is genuinely double-entry, rates come from a live engine with a static fallback, refunds are real for Ethereum checkout sessions, and sandbox mode is real — but none of this layer has been through a live-network verification pass, deposit monitoring on Payment Channels does not exist, and the hosted checkout widget has never been proven against a real transaction. Do not point production traffic at it yet.",
    "operator_route_note": "Tenant creation, API-key issuance/revocation, wallet registration, chain onboarding, limits, and operator controls are private admin operations. They are not a public integration surface and require FastPay staff authorization."
  },
  "conventions": {
    "sandbox": "Sandbox tenants are scoped to testnet: every payment link they create settles on Ethereum Sepolia in testnet tokens (Sepolia USDC 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238, or native test ETH), the hosted checkout switches the wallet to Sepolia, and confirmation is verified against a Sepolia RPC — so an end-to-end payment can be run with faucet funds and no real value ever moves. Sepolia has no canonical USDT, so USDT links are rejected in the sandbox. An fp_test_ key routes checkout and refund building/confirming through a fully simulated adapter: no real chain call, no real-world address validation. Two reserved confirm hashes control the outcome — 0x followed by 64 f characters forces failed, 0x followed by 64 e characters forces pending, and any other well-formed hash succeeds. Everything downstream is real: a sandbox payment that reaches paid fires the real webhook and posts to the real double-entry ledger. Sandbox covers the Payment Gateway's anonymous checkout and refund routes only; the mass disbursement and relay mass-payout APIs have no sandbox equivalent.",
    "versioning_pagination_events": "Every response carries an X-API-Version header, and the /v1/ surface is covered by an explicit no-breaking-changes policy. Relay list endpoints use Stripe-style cursor pagination (limit, starting_after, has_more) and every relay error body includes a request_id. Relay errors have converged on one envelope: error_type, error_slug and error_message. Webhooks carry a stable, dedupe-safe event id and a dot-namespaced type such as payment.completed, on both the relay and the disbursement engine. A published payment lifecycle (six canonical states, sourced from the state machine in code), per-route rate-limit numbers — including the routes with no limit enforced today — and a changelog of externally visible API changes are maintained alongside this reference."
  },
  "authentication": [
    {
      "name": "Relay tenant key",
      "header": "Authorization: Bearer fp_live_…",
      "usage": "Mass payout and merchant-authenticated checkout routes",
      "failure_behavior": "401 with missing_api_key or invalid_api_key"
    },
    {
      "name": "Sandbox tenant key",
      "header": "Authorization: Bearer fp_test_…",
      "usage": "Payment Gateway checkout and refund routes on Ethereum Sepolia testnet, or against a simulated chain adapter",
      "failure_behavior": "Same envelope as a live key; only the on-chain interaction is faked"
    },
    {
      "name": "Engine/admin key",
      "header": "x-admin-api-key",
      "usage": "Custodial engine and operator-only administration",
      "failure_behavior": "403 when absent or invalid"
    },
    {
      "name": "Public checkout",
      "header": "None",
      "usage": "Public request read, intent, and confirmation",
      "failure_behavior": "The unguessable request id is the capability token"
    }
  ],
  "limits": [
    {
      "name": "Engine payments per request",
      "value": "500",
      "configuration": "MAX_PAYMENTS_PER_REQUEST"
    },
    {
      "name": "Relay payments per request",
      "value": "50",
      "configuration": "RELAY_MAX_PAYMENTS_PER_REQUEST"
    },
    {
      "name": "Amount per payment",
      "value": "1,000,000",
      "configuration": "MAX_AMOUNT_PER_PAYMENT"
    },
    {
      "name": "Engine daily payment count",
      "value": "2,000 / UTC day",
      "configuration": "DAILY_PAYMENT_COUNT_CAP"
    },
    {
      "name": "Tenant daily payment count",
      "value": "Operator configured",
      "configuration": "Per-tenant setting"
    },
    {
      "name": "Public checkout routes",
      "value": "30 requests / minute / IP",
      "configuration": "RELAY_PUBLIC_PAYMENT_REQUEST_RATE_LIMIT"
    },
    {
      "name": "Engine request body",
      "value": "~500 × 600B + 16KB",
      "configuration": "MAX_REQUEST_BODY_BYTES"
    },
    {
      "name": "List page size",
      "value": "Default and maximum are configured",
      "configuration": "PAGINATION_DEFAULT_LIMIT / PAGINATION_MAX_LIMIT"
    },
    {
      "name": "Reconciliation window",
      "value": "Operator configured",
      "configuration": "RELAY_RECONCILIATION_DEFAULT_WINDOW_DAYS"
    }
  ],
  "chains": [
    {
      "chain": "Ethereum",
      "assets": [
        "ETH",
        "USDC",
        "USDT"
      ],
      "availability": "Engine + relay; hosted checkout is Ethereum-only"
    },
    {
      "chain": "Solana",
      "assets": [
        "SOL",
        "USDC",
        "USDT"
      ],
      "availability": "Engine + relay verification; checkout fulfillment is not enabled"
    },
    {
      "chain": "Tron",
      "assets": [
        "TRX",
        "USDT"
      ],
      "availability": "Engine + relay; direct-transfer behavior, no contract fee split"
    },
    {
      "chain": "Base",
      "assets": [
        "ETH",
        "USDC"
      ],
      "availability": "Custodial engine coverage"
    },
    {
      "chain": "BNB Chain",
      "assets": [
        "BNB",
        "USDC"
      ],
      "availability": "Custodial engine coverage"
    },
    {
      "chain": "Arbitrum",
      "assets": [
        "ETH"
      ],
      "availability": "Engine coverage; native asset only"
    },
    {
      "chain": "Polygon",
      "assets": [
        "POL"
      ],
      "availability": "Engine coverage; native asset only"
    }
  ],
  "endpoint_groups": [
    {
      "title": "Disbursement engine",
      "description": "FastPay-funded, custodial disbursements. FastPay operates the wallet and signs the transaction; this is separate from the client-funded relay gateway.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/health",
          "title": "Health check",
          "auth": "none",
          "summary": "Checks that the service and its broadcast ledger are reachable. It does not prove RPC or bundler connectivity.",
          "response_example": "{\n  \"status\": \"ok\",\n  \"ledger\": \"ok\",\n  \"paused\": false\n}"
        },
        {
          "method": "POST",
          "path": "/disburse",
          "title": "Trigger a disbursement",
          "auth": "admin",
          "summary": "Accepts an asynchronous batch funded from FastPay's custodial wallet and returns a job identifier.",
          "notes": [
            "Maximum 500 payments by default. ALLOCATION_ID is the engine's deduplication key.",
            "The response is not the payment result; consume webhooks for terminal outcomes."
          ],
          "request_example": "{\n  \"payments\": [\n    {\n      \"ALLOCATION_ID\": \"payroll-2026-09-0001\",\n      \"CHAIN\": \"ETH\",\n      \"WALLET\": \"0x1111111111111111111111111111111111111111\",\n      \"CURRENCY\": \"USDC\",\n      \"AMOUNT\": 1250.00\n    }\n  ],\n  \"callbackUrl\": \"https://client.example.com/webhooks/fastpay\",\n  \"resetDeduplication\": false\n}",
          "response_example": "{\n  \"jobId\": \"b3f1...\",\n  \"payments_accepted\": 1,\n  \"duplicates_skipped\": 0,\n  \"skipped_allocations\": []\n}"
        },
        {
          "method": "POST",
          "path": "/cancel",
          "title": "Cancel a job",
          "auth": "admin",
          "summary": "Stops work that has not yet been broadcast. It cannot recall an on-chain transaction and is in-memory only.",
          "request_example": "{\n  \"job_id\": \"b3f1...\"\n}",
          "response_example": "{ \"success\": true, \"message\": \"Job cancelled.\" }"
        },
        {
          "method": "GET",
          "path": "/status/:jobId",
          "title": "Get job status",
          "auth": "none",
          "summary": "Returns a best-effort progress snapshot for a job. Webhooks remain the authoritative delivery mechanism.",
          "response_example": "{\n  \"jobId\": \"b3f1...\",\n  \"status\": \"running\",\n  \"progress\": { \"total\": 10, \"completed\": 4, \"failed\": 0, \"pending\": 6 }\n}"
        },
        {
          "method": "GET",
          "path": "/payments/:allocationId",
          "title": "Get a payment by allocation id",
          "auth": "admin",
          "summary": "Reads the engine's own durable money-safety stores (deduplication table and broadcast ledger) for a single ALLOCATION_ID.",
          "notes": [
            "A failure reason is deliberately not returned here; that detail is only ever sent once, on the webhook, and is not durably persisted by the engine."
          ],
          "response_example": "{\n  \"allocation_id\": \"payroll-2026-09-0001\",\n  \"status\": \"completed\",\n  \"chain\": \"ETH\",\n  \"wallet\": \"0x...\",\n  \"tx_hash\": \"0x...\"\n}"
        },
        {
          "method": "GET",
          "path": "/payments",
          "title": "List payments",
          "auth": "admin",
          "summary": "Enumerates every allocation the engine has accepted, using the same cursor pagination as the relay list endpoints.",
          "notes": [
            "Pagination is limit / starting_after / has_more. Defaults come from PAGINATION_DEFAULT_LIMIT and PAGINATION_MAX_LIMIT."
          ],
          "response_example": "{\n  \"object\": \"list\",\n  \"data\": [{ \"allocation_id\": \"payroll-2026-09-0001\", \"status\": \"completed\" }],\n  \"has_more\": false\n}"
        },
        {
          "method": "POST",
          "path": "/admin/pause · /admin/resume",
          "title": "Pause or resume intake",
          "auth": "admin",
          "summary": "Operator kill switch that blocks new jobs without touching work already in flight.",
          "response_example": "{ \"success\": true, \"paused\": true, \"pausedAt\": \"2026-09-02T10:00:00Z\" }"
        },
        {
          "method": "POST",
          "path": "/admin/reset · /admin/remove-processed-ids",
          "title": "Manage deduplication",
          "auth": "admin",
          "summary": "Reset all tracked allocation IDs or surgically remove named IDs so they can be admitted again.",
          "response_example": "{ \"success\": true, \"removed\": [\"alloc-123\"], \"notFound\": [] }"
        },
        {
          "method": "GET",
          "path": "/admin/stats",
          "title": "Get operational stats",
          "auth": "admin",
          "summary": "Returns deduplication counters and configured connector usage for operator monitoring.",
          "response_example": "{\n  \"success\": true,\n  \"deduplication\": { \"processedCount\": 1204, \"processingCount\": 2, \"totalTracked\": 1206 },\n  \"timestamp\": \"2026-09-02T10:00:00Z\"\n}"
        }
      ]
    },
    {
      "title": "Relay · mass payout",
      "description": "Client-funded, non-custodial payouts. A tenant's own smart account funds the transfer and grants FastPay a bounded permission to submit it; no private key is transferred.",
      "endpoints": [
        {
          "method": "POST",
          "path": "/v1/instructions",
          "title": "Create instructions",
          "auth": "bearer",
          "summary": "Accepts a batch for asynchronous execution and returns immediately with a job id.",
          "notes": [
            "Relay batches are capped at 50 by default.",
            "network is a body field: ethereum, tron, or solana. Ethereum is token-only on this unattended path; native ETH is rejected.",
            "A tenant daily payment cap may return 402 daily_payment_cap_exceeded."
          ],
          "request_example": "{\n  \"network\": \"ethereum\",\n  \"payments\": [\n    {\n      \"ALLOCATION_ID\": \"payroll-2026-09-0001\",\n      \"CHAIN\": \"ETH\",\n      \"WALLET\": \"0x1111111111111111111111111111111111111111\",\n      \"CURRENCY\": \"USDC\",\n      \"AMOUNT\": 1250.00\n    }\n  ],\n  \"callbackUrl\": \"https://client.example.com/webhooks/fastpay\"\n}",
          "response_example": "{ \"jobId\": \"bc163be2-0979-4a50-bd71-4b87445fe627\", \"status\": \"accepted\" }"
        },
        {
          "method": "GET",
          "path": "/v1/payouts/:payoutId",
          "title": "Get a payout",
          "auth": "bearer",
          "summary": "Returns one payout — an individual entry inside a mass-payout job, addressable as its own resource.",
          "response_example": "{\n  \"id\": \"po_...\",\n  \"object\": \"payout\",\n  \"job_id\": \"bc163be2-0979-4a50-bd71-4b87445fe627\",\n  \"status\": \"completed\",\n  \"network\": \"ethereum\",\n  \"currency\": \"USDC\",\n  \"amount\": \"1250.00\",\n  \"wallet\": \"0x...\",\n  \"tx_hash\": \"0x...\"\n}"
        },
        {
          "method": "GET",
          "path": "/v1/jobs/:jobId/payouts",
          "title": "List payouts for a job",
          "auth": "bearer",
          "summary": "Lists the payout entries belonging to one job, newest first, with cursor pagination.",
          "notes": [
            "Use limit and starting_after to page. These endpoints no longer return an entire history in one response."
          ],
          "response_example": "{\n  \"object\": \"list\",\n  \"data\": [{ \"id\": \"po_...\", \"object\": \"payout\", \"status\": \"completed\" }],\n  \"has_more\": false\n}"
        },
        {
          "method": "GET",
          "path": "/v1/jobs/:jobId",
          "title": "Get a payout job",
          "auth": "bearer",
          "summary": "Returns the tenant-scoped job, lifecycle status, and per-payment results for polling or a status page.",
          "response_example": "{\n  \"jobId\": \"bc163be2-0979-4a50-bd71-4b87445fe627\",\n  \"status\": \"processing\",\n  \"network\": \"ethereum\",\n  \"payments\": [{ \"ALLOCATION_ID\": \"payroll-2026-09-0001\", \"status\": \"completed\", \"TX_HASH\": \"0x...\" }]\n}"
        },
        {
          "method": "POST",
          "path": "/v1/jobs/:jobId/cancel",
          "title": "Cancel a payout job",
          "auth": "bearer",
          "summary": "Cancels a job only while it is accepted or awaiting_onboarding. Once execution starts, the job is not cancellable.",
          "notes": [
            "A successful cancellation does not undo anything already broadcast."
          ],
          "response_example": "{ \"jobId\": \"bc163be2-0979-4a50-bd71-4b87445fe627\", \"status\": \"cancelled\" }"
        }
      ]
    },
    {
      "title": "Relay · checkout",
      "description": "A merchant creates a payment request and the payer's own wallet signs and broadcasts. Public read, intent, and confirmation routes do not require a tenant key; the request id is the capability token.",
      "endpoints": [
        {
          "method": "POST",
          "path": "/v1/payment-requests",
          "title": "Create a payment request",
          "auth": "bearer",
          "summary": "Creates a single-use invoice-like payment request with a future expiry of up to seven days.",
          "notes": [
            "Use Idempotency-Key when supported by the deployed relay; same key and body is replay-safe, while a different body returns 409.",
            "The backend payment-request flow is available for Ethereum and Tron; the hosted page and SDK currently execute Ethereum only."
          ],
          "request_example": "{\n  \"reference\": \"order-1042\",\n  \"network\": \"ethereum\",\n  \"currency\": \"USDC\",\n  \"amount\": \"250.00\",\n  \"expiresAt\": \"2026-09-05T12:00:00Z\",\n  \"recipient\": \"0x1111111111111111111111111111111111111111\",\n  \"callbackUrl\": \"https://merchant.example.com/fastpay\"\n}",
          "response_example": "{\n  \"id\": \"pr_...\",\n  \"reference\": \"order-1042\",\n  \"status\": \"pending\",\n  \"amount\": \"250.00\",\n  \"expiresAt\": \"2026-09-05T12:00:00Z\"\n}"
        },
        {
          "method": "GET",
          "path": "/v1/payment-requests/:requestId",
          "title": "Get a payment request",
          "auth": "bearer",
          "summary": "Returns the merchant's full tenant-scoped payment-request record.",
          "response_example": "{ \"id\": \"pr_...\", \"reference\": \"order-1042\", \"network\": \"ethereum\", \"currency\": \"USDC\", \"amount\": \"250.00\", \"status\": \"pending\" }"
        },
        {
          "method": "GET",
          "path": "/v1/payment-requests/:requestId/public",
          "title": "Get a public payment request",
          "auth": "none",
          "summary": "Returns the sanitized fields needed by a hosted checkout, without tenant ids, callback secrets, or private metadata.",
          "notes": [
            "Only http and https redirect URLs are exposed; unsafe or missing redirects become null."
          ],
          "response_example": "{\n  \"id\": \"pr_...\",\n  \"reference\": \"order-1042\",\n  \"network\": \"ethereum\",\n  \"currency\": \"USDC\",\n  \"amount\": \"250.00\",\n  \"recipient\": \"0x...\",\n  \"status\": \"pending\",\n  \"redirectUrl\": null\n}"
        },
        {
          "method": "POST",
          "path": "/v1/payment-requests/:requestId/intent",
          "title": "Create a payment intent",
          "auth": "none",
          "summary": "Builds unsigned transaction data for the payer's connected wallet. The wallet, not FastPay, signs and broadcasts.",
          "notes": [
            "An approval transaction may be returned before a token transfer when allowance is insufficient.",
            "Solana intent execution is not enabled in the current hosted checkout."
          ],
          "request_example": "{ \"payerAddress\": \"0x2222222222222222222222222222222222222222\" }",
          "response_example": "{\n  \"paymentRequestId\": \"pr_...\",\n  \"chainId\": 1,\n  \"to\": \"0x...\",\n  \"data\": \"0x...\",\n  \"value\": \"0x0\",\n  \"approval\": null,\n  \"fee\": \"1.25\",\n  \"total\": \"251.25\"\n}"
        },
        {
          "method": "POST",
          "path": "/v1/payment-requests/:requestId/confirm",
          "title": "Confirm a payment",
          "auth": "none",
          "summary": "Verifies the exact on-chain transfer before marking the request paid; the reported hash is never trusted by itself.",
          "notes": [
            "A pending chain confirmation returns 202 and should be retried; a mismatch returns 422 payment_not_verified.",
            "The first successful confirmation emits the checkout webhook when a callback URL was configured."
          ],
          "request_example": "{ \"txHash\": \"0x9f2b7c4d81e0a3f65c2b9d7e4a10f83b6c5d24e97a0b1c3d5e7f9a2b4c6d8e01\" }",
          "response_example": "{ \"status\": \"paid\", \"txHash\": \"0x...\", \"verification\": \"verified\" }"
        }
      ]
    },
    {
      "title": "Relay · Checkout Sessions (beta)",
      "beta": true,
      "description": "Beta reference: these surfaces are documented from the uploaded FastPay API Reference, but they are not part of the current public OpenAPI contract. Do not send production funds against them until FastPay confirms a deployed URL and completes live-network verification.",
      "endpoints": [
        {
          "method": "POST",
          "path": "/v1/checkout/sessions",
          "title": "Create a checkout session",
          "auth": "bearer",
          "beta": true,
          "summary": "Creates a fiat-denominated session, resolves the tenant's registered payout wallet, and quotes a supported settlement currency.",
          "notes": [
            "currency is EUR or USD. settlement_currency must have a rate available; unsupported quotes are rejected rather than priced 1:1.",
            "Rates come from a live CoinGecko-backed engine with a recent-cache fallback and an operator static table as a last resort, which is labelled as a fallback rather than presented as live.",
            "callback_url is optional and SSRF-checked. When set, a checkout_session-shaped paid webhook fires on confirmation, carrying checkout_session_id, payment_id and net_settlement — not the payment_request shape used by the mature checkout flow.",
            "Recipient resolution uses the tenant's registered payout wallet; it is never client-supplied or guessed. The ledger credits the fee-net settlement amount, not the gross charge."
          ],
          "request_example": "{\n  \"amount\": \"100.00\",\n  \"currency\": \"EUR\",\n  \"network\": \"ethereum\",\n  \"settlement_currency\": \"USDC\",\n  \"success_url\": \"https://merchant.example/success\",\n  \"cancel_url\": \"https://merchant.example/cancel\",\n  \"callback_url\": \"https://merchant.example/webhooks/fastpay\",\n  \"merchant_reference\": \"order-1042\",\n  \"customer\": { \"id\": \"cus_...\" }\n}",
          "response_example": "{\n  \"id\": \"cs_...\", \"object\": \"checkout_session\", \"status\": \"open\", \"payment_status\": \"created\",\n  \"amount\": \"100.00\", \"currency\": \"EUR\", \"network\": \"ethereum\",\n  \"payment_amount\": \"108.000000\", \"payment_currency\": \"USDC\", \"exchange_rate\": \"1.08\",\n  \"payment_url\": \"https://relay.fastpay.app/checkout/index.html?id=cs_...\",\n  \"expires_at\": \"...\", \"created_at\": \"...\"\n}"
        },
        {
          "method": "GET",
          "path": "/v1/checkout/sessions/:id",
          "title": "Get a checkout session",
          "auth": "bearer",
          "beta": true,
          "summary": "Returns a session and its linked payments.",
          "notes": [
            "Use the linked pay_... id with GET /v1/payments/:id; it is not the same resource as the session."
          ],
          "response_example": "{\n  \"id\": \"cs_...\", \"object\": \"checkout_session\", \"status\": \"open\",\n  \"amount\": \"100.00\", \"currency\": \"EUR\",\n  \"payments\": [{ \"id\": \"pay_...\", \"status\": \"created\", \"merchant_reference\": \"order-1042\" }]\n}"
        },
        {
          "method": "POST",
          "path": "/v1/checkout/sessions/:id/expire",
          "title": "Expire a checkout session",
          "auth": "bearer",
          "beta": true,
          "summary": "Closes an open checkout session.",
          "response_example": "{ \"id\": \"cs_...\", \"object\": \"checkout_session\", \"status\": \"expired\" }"
        }
      ]
    },
    {
      "title": "Payments (beta)",
      "beta": true,
      "description": "A full lifecycle view for one payment, including immutable status history, financial fields, blockchain data, and related resource ids.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/v1/payments/:id",
          "title": "Get a payment",
          "auth": "bearer",
          "beta": true,
          "summary": "Returns more than the current status: every recorded transition, the financial breakdown, known chain data, and related objects.",
          "notes": [
            "lifecycle is backed by insert-only history, not synthesized.",
            "settlementType is always ledger_credit today: crypto moves payer → merchant wallet non-custodially, and the ledger posting is bookkeeping, not a balance FastPay holds. An auto-converted fiat balance and an external bank payout are not built."
          ],
          "response_example": "{\n  \"object\": \"payment\", \"id\": \"pay_...\", \"status\": \"paid\",\n  \"lifecycle\": [{ \"status\": \"created\", \"at\": \"2026-09-02T10:00:00.000Z\" }, { \"status\": \"paid\", \"at\": \"2026-09-02T10:04:12.000Z\" }],\n  \"financial\": { \"originalAmount\": \"100.00\", \"originalCurrency\": \"EUR\", \"paymentAmount\": \"108.000000\", \"paymentCurrency\": \"USDC\", \"exchangeRate\": \"1.08\", \"gatewayFee\": \"1.00\", \"networkFee\": \"1.5\", \"conversionFee\": \"0.20\", \"netSettlement\": \"99.00\", \"settlementCurrency\": \"EUR\", \"settlementType\": \"ledger_credit\" },\n  \"blockchain\": { \"network\": \"ethereum\", \"asset\": \"USDC\", \"address\": \"0x...\", \"txHash\": \"0xdeadbeef...\", \"confirmations\": 12 },\n  \"related\": { \"checkoutSessionId\": \"cs_...\", \"customerId\": null, \"refundIds\": [\"re_...\"], \"ledgerTransactionIds\": [\"le_...\"] }\n}"
        },
        {
          "method": "GET",
          "path": "/v1/payments/:id/transactions",
          "title": "List payment transactions",
          "auth": "bearer",
          "beta": true,
          "summary": "Exposes the on-chain transaction records already stored for a payment, including real Ethereum confirmation counts and reorg detection.",
          "notes": [
            "Confirmation monitoring is implemented for Ethereum. A reorged transaction is surfaced rather than silently left as confirmed."
          ],
          "response_example": "{\n  \"object\": \"list\",\n  \"data\": [{ \"txHash\": \"0x...\", \"network\": \"ethereum\", \"confirmations\": 12, \"status\": \"confirmed\", \"reorged\": false }]\n}"
        }
      ]
    },
    {
      "title": "Refunds (beta)",
      "beta": true,
      "description": "Non-custodial refunds for a checkout-session-originated payment. FastPay never held the funds, so a refund is an unsigned transaction the merchant's own wallet signs and broadcasts.",
      "endpoints": [
        {
          "method": "POST",
          "path": "/v1/payments/:id/refunds",
          "title": "Create a refund",
          "auth": "bearer",
          "beta": true,
          "summary": "Builds an unsigned refund transaction for the merchant's wallet. Partial refunds are supported.",
          "notes": [
            "Ethereum only, and only for a payment created through a checkout session. The older standalone payment-request flow has nothing to attach a refund to."
          ],
          "request_example": "{ \"amount\": \"25.00\", \"reason\": \"customer_request\" }",
          "response_example": "{\n  \"id\": \"re_...\", \"object\": \"refund\", \"payment_id\": \"pay_...\", \"status\": \"pending\",\n  \"amount\": \"25.00\", \"currency\": \"EUR\",\n  \"transaction\": { \"to\": \"0x...\", \"data\": \"0x...\", \"value\": \"0x0\", \"chainId\": 1 }\n}"
        },
        {
          "method": "GET",
          "path": "/v1/refunds/:id",
          "title": "Get a refund",
          "auth": "bearer",
          "beta": true,
          "summary": "Returns one tenant-scoped refund and its current state.",
          "response_example": "{ \"id\": \"re_...\", \"object\": \"refund\", \"payment_id\": \"pay_...\", \"status\": \"confirmed\", \"amount\": \"25.00\", \"tx_hash\": \"0x...\" }"
        },
        {
          "method": "POST",
          "path": "/v1/refunds/:id/confirm",
          "title": "Confirm a refund",
          "auth": "bearer",
          "beta": true,
          "summary": "Verifies the broadcast refund on-chain and posts the matching ledger debit once confirmed.",
          "notes": [
            "The full refund lifecycle is written to the admin audit trail."
          ],
          "request_example": "{ \"txHash\": \"0x...\" }",
          "response_example": "{ \"id\": \"re_...\", \"status\": \"confirmed\", \"tx_hash\": \"0x...\" }"
        }
      ]
    },
    {
      "title": "Balance & Ledger (beta)",
      "beta": true,
      "description": "A real double-entry balance and ledger API. Every payment or refund posts two balanced legs under one transaction id — the merchant's payable balance and the exact mirror entry on FastPay's internal settlement account. It is still a two-account chart, not a general ledger.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/v1/balance",
          "title": "List balances",
          "auth": "bearer",
          "beta": true,
          "summary": "Lists each currency for which the tenant has a ledger account.",
          "notes": [
            "Balances are derived from balanced ledger legs and are credited net of FastPay's gateway fee, not gross."
          ],
          "response_example": "{\n  \"object\": \"list\",\n  \"data\": [{ \"currency\": \"EUR\", \"available\": \"99.00\", \"pending\": \"0.00\", \"total\": \"99.00\" }]\n}"
        },
        {
          "method": "GET",
          "path": "/v1/balance/:currency",
          "title": "Get a balance",
          "auth": "bearer",
          "beta": true,
          "summary": "Returns one currency balance, creating the account on first read when needed.",
          "response_example": "{ \"currency\": \"EUR\", \"available\": \"99.00\", \"pending\": \"0.00\", \"total\": \"99.00\" }"
        },
        {
          "method": "GET",
          "path": "/v1/balance/transactions",
          "title": "List ledger transactions",
          "auth": "bearer",
          "beta": true,
          "summary": "Lists immutable ledger entries for the tenant, most recent first, with cursor pagination and an optional CSV export.",
          "notes": [
            "?format=csv returns a reconciliation-friendly export of the same rows.",
            "Paginate with limit and starting_after; has_more indicates a further page."
          ],
          "response_example": "{\n  \"object\": \"list\",\n  \"data\": [{ \"id\": \"le_...\", \"object\": \"ledger_transaction\", \"transaction_id\": \"txn_...\", \"direction\": \"credit\", \"amount\": \"99.00\", \"currency\": \"EUR\", \"balance_before\": \"0.00\", \"balance_after\": \"99.00\", \"reference_type\": \"payment\", \"reference_id\": \"pay_...\", \"created_at\": \"...\" }],\n  \"has_more\": false\n}"
        },
        {
          "method": "GET",
          "path": "/v1/reconciliation-report",
          "title": "Reconciliation report",
          "auth": "bearer",
          "beta": true,
          "summary": "Cross-references business state, on-chain state, and ledger state for paid payments and flags where the three disagree, including reorgs.",
          "notes": [
            "The default window comes from RELAY_RECONCILIATION_DEFAULT_WINDOW_DAYS."
          ],
          "response_example": "{\n  \"object\": \"reconciliation_report\",\n  \"window_days\": 7,\n  \"checked\": 128,\n  \"discrepancies\": [{ \"payment_id\": \"pay_...\", \"reason\": \"onchain_reorged\" }]\n}"
        },
        {
          "method": "GET",
          "path": "/v1/webhook-deliveries",
          "title": "List webhook deliveries",
          "auth": "bearer",
          "beta": true,
          "summary": "Shows every webhook this gateway has attempted to send you, paginated, with the detail route returning the full payload.",
          "notes": [
            "Each attempt records the receiver's real HTTP status code and a capped response body; both are null when no response was ever received (a network failure, not a receiver error)."
          ],
          "response_example": "{\n  \"object\": \"list\",\n  \"data\": [{ \"id\": \"whd_...\", \"event_id\": \"evt_...\", \"type\": \"payment.completed\", \"status\": \"delivered\", \"attempts\": 1, \"response_status\": 200, \"response_body\": \"ok\" }],\n  \"has_more\": false\n}"
        },
        {
          "method": "POST",
          "path": "/v1/webhook-deliveries/:deliveryId/redeliver",
          "title": "Redeliver a webhook",
          "auth": "bearer",
          "beta": true,
          "summary": "Retries a specific delivery on demand instead of waiting for the periodic sweep.",
          "notes": [
            "Safe to call on an already-delivered row — your endpoint simply receives the same event id again.",
            "Returns 409 if a redelivery for that row is already in flight, so the webhook is never double-sent."
          ],
          "response_example": "{\n  \"id\": \"whd_...\",\n  \"status\": \"pending\",\n  \"attempts\": 2\n}"
        }
      ]
    },
    {
      "title": "Customers & Payment Channels (beta)",
      "beta": true,
      "description": "Lightweight customer records and server-generated deposit addresses. Addresses are real, but deposit detection and automatic ledger linking are not implemented.",
      "endpoints": [
        {
          "method": "POST",
          "path": "/v1/customers",
          "title": "Create a customer",
          "auth": "bearer",
          "beta": true,
          "summary": "Creates a lightweight customer record; this is not a KYC or identity object.",
          "notes": [
            "email only receives a basic @ format check; country is free text and is not a compliance or eligibility check."
          ],
          "request_example": "{ \"email\": \"payer@example.com\", \"name\": \"Example Payer\", \"country\": \"US\" }",
          "response_example": "{ \"id\": \"cus_...\", \"object\": \"customer\", \"email\": \"payer@example.com\", \"name\": \"Example Payer\", \"country\": \"US\", \"created_at\": \"...\" }"
        },
        {
          "method": "GET",
          "path": "/v1/customers/:id",
          "title": "Get a customer",
          "auth": "bearer",
          "beta": true,
          "summary": "Returns one tenant-scoped customer record.",
          "response_example": "{ \"id\": \"cus_...\", \"object\": \"customer\", \"email\": \"payer@example.com\", \"name\": \"Example Payer\", \"country\": \"US\", \"created_at\": \"...\" }"
        },
        {
          "method": "GET",
          "path": "/v1/customers",
          "title": "List customers",
          "auth": "bearer",
          "beta": true,
          "summary": "Lists customer records for the tenant.",
          "response_example": "{ \"object\": \"list\", \"data\": [{ \"id\": \"cus_...\", \"object\": \"customer\", \"email\": \"payer@example.com\" }] }"
        },
        {
          "method": "POST",
          "path": "/v1/payment-channels",
          "title": "Create a payment channel",
          "auth": "bearer",
          "beta": true,
          "summary": "Mints one dedicated address per requested network for a customer.",
          "notes": [
            "Addresses are generated server-side through Turnkey and never accepted from the caller.",
            "Generated does not mean monitored: no chain watcher currently detects deposits, links them to the customer, updates the ledger, or emits a deposit webhook."
          ],
          "request_example": "{\n  \"customer_id\": \"cus_...\",\n  \"networks\": [\"ethereum\", \"tron\"]\n}",
          "response_example": "{\n  \"id\": \"pc_...\", \"object\": \"payment_channel\", \"customer_id\": \"cus_...\", \"status\": \"active\",\n  \"addresses\": [{ \"currency\": \"ETHEREUM\", \"network\": \"ethereum\", \"address\": \"0x...\" }, { \"currency\": \"TRON\", \"network\": \"tron\", \"address\": \"T...\" }],\n  \"created_at\": \"...\"\n}"
        },
        {
          "method": "GET",
          "path": "/v1/payment-channels/:id",
          "title": "Get a payment channel",
          "auth": "bearer",
          "beta": true,
          "summary": "Returns one tenant-scoped payment channel and its addresses.",
          "response_example": "{ \"id\": \"pc_...\", \"object\": \"payment_channel\", \"customer_id\": \"cus_...\", \"status\": \"active\", \"addresses\": [{ \"network\": \"ethereum\", \"address\": \"0x...\" }] }"
        },
        {
          "method": "GET",
          "path": "/v1/payment-channels",
          "title": "List payment channels",
          "auth": "bearer",
          "beta": true,
          "summary": "Lists payment channels for the tenant.",
          "response_example": "{ \"object\": \"list\", \"data\": [{ \"id\": \"pc_...\", \"object\": \"payment_channel\", \"customer_id\": \"cus_...\", \"status\": \"active\" }] }"
        },
        {
          "method": "POST",
          "path": "/v1/payment-channels/:id/close",
          "title": "Close a payment channel",
          "auth": "bearer",
          "beta": true,
          "summary": "Closes a payment channel so it is no longer active.",
          "response_example": "{ \"id\": \"pc_...\", \"object\": \"payment_channel\", \"status\": \"closed\" }"
        }
      ]
    },
    {
      "title": "Discovery & rates",
      "description": "Public, unauthenticated endpoints for discovering what the API accepts and what an amount converts to. Rates come from the same live pipeline Checkout Sessions quote with — they are indicative, not a locked quote.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/v1/networks",
          "title": "List networks",
          "auth": "none",
          "summary": "Lists every network the API validates a request for, with an honest live flag separating request-validation support from fulfillment support.",
          "notes": [
            "solana reports live: false today: a payment request for it validates, but checkout fulfillment still rejects it."
          ],
          "response_example": "{\n  \"object\": \"list\",\n  \"data\": [\n    { \"network\": \"ethereum\", \"live\": true },\n    { \"network\": \"tron\", \"live\": true },\n    { \"network\": \"solana\", \"live\": false }\n  ]\n}"
        },
        {
          "method": "GET",
          "path": "/v1/currencies",
          "title": "List currencies",
          "auth": "none",
          "summary": "Lists the assets accepted per network, using the same live flag semantics as /v1/networks.",
          "response_example": "{\n  \"object\": \"list\",\n  \"data\": [{ \"currency\": \"USDC\", \"network\": \"ethereum\", \"decimals\": 6, \"live\": true }]\n}"
        },
        {
          "method": "GET",
          "path": "/v1/rates?from=EUR&to=USDC",
          "title": "Get a rate",
          "auth": "none",
          "summary": "Returns the current indicative rate from the live rate engine used internally by Checkout Sessions.",
          "notes": [
            "Not a locked quote: only a checkout session locks a rate for its own lifetime.",
            "Not a second rate source — it wraps the same pipeline the checkout quote uses."
          ],
          "response_example": "{\n  \"object\": \"rate\", \"from\": \"EUR\", \"to\": \"USDC\", \"rate\": \"1.08\", \"as_of\": \"2026-09-04T10:00:00.000Z\", \"locked\": false\n}"
        }
      ]
    },
    {
      "title": "Relay · admin & tenants",
      "operatorOnly": true,
      "description": "Operator-run setup for tenants, API keys, wallets, limits, and chain-specific onboarding. These routes are not a merchant-facing integration surface.",
      "endpoints": [
        {
          "method": "POST",
          "path": "/admin/tenants",
          "title": "Create a tenant",
          "auth": "admin",
          "operatorOnly": true,
          "summary": "Creates a tenant and returns its webhook secret once.",
          "request_example": "{ \"name\": \"Example business\" }",
          "response_example": "{ \"id\": \"tn_...\", \"name\": \"Example business\", \"status\": \"active\", \"webhookSecret\": \"whsec_...\" }"
        },
        {
          "method": "POST",
          "path": "/admin/tenants/:id/api-keys",
          "title": "Issue an API key",
          "auth": "admin",
          "operatorOnly": true,
          "summary": "Issues a tenant Bearer key; the raw key is returned once and stored hashed at rest.",
          "notes": [
            "API keys are passwords. Never put the raw value in browser code, source control, logs, or support tickets."
          ],
          "request_example": "{ \"test\": false }",
          "response_example": "{ \"id\": \"key_...\", \"apiKey\": \"fp_live_...\", \"prefix\": \"fp_live_\", \"warning\": \"Store this key now; it will not be shown again.\" }"
        },
        {
          "method": "POST",
          "path": "/admin/tenants/:id/api-keys/:keyId/revoke",
          "title": "Revoke an API key",
          "auth": "admin",
          "operatorOnly": true,
          "summary": "Revokes a tenant key so subsequent Bearer requests fail authentication.",
          "response_example": "{ \"success\": true }"
        },
        {
          "method": "POST",
          "path": "/admin/tenants/:id/wallets",
          "title": "Register a wallet",
          "auth": "admin",
          "operatorOnly": true,
          "summary": "Registers a validated payout or receiving address for a supported network.",
          "notes": [
            "chain uses lowercase ethereum, solana, or tron. Do not send abbreviations such as eth; the address is validated for the selected chain."
          ],
          "request_example": "{ \"chain\": \"ethereum\", \"address\": \"0x1111111111111111111111111111111111111111\" }",
          "response_example": "{ \"success\": true, \"tenantId\": \"tn_...\", \"chain\": \"ethereum\", \"address\": \"0x...\" }"
        },
        {
          "method": "GET / DELETE",
          "path": "/admin/tenants/:id/wallets[/:chain]",
          "title": "List or remove wallets",
          "auth": "admin",
          "operatorOnly": true,
          "summary": "Lists registered wallets or removes a wallet for a tenant and optional chain.",
          "response_example": "{ \"success\": true, \"tenantId\": \"tn_...\", \"wallets\": [{ \"chain\": \"ethereum\", \"address\": \"0x...\" }] }"
        },
        {
          "method": "POST",
          "path": "/admin/tenants/:id/status · /admin/tenants/:id/limits",
          "title": "Set tenant status and limits",
          "auth": "admin",
          "operatorOnly": true,
          "summary": "Suspends or reactivates a tenant and configures request or daily payment-count limits.",
          "request_example": "{ \"status\": \"active\" }\n\n{ \"requestsPerMinute\": 60, \"dailyPaymentCountCap\": 2000 }",
          "response_example": "{ \"success\": true }"
        },
        {
          "method": "POST",
          "path": "/admin/tenants/:id/onboard · .../tron/onboard · .../solana/onboard",
          "title": "Complete chain onboarding",
          "auth": "admin",
          "operatorOnly": true,
          "summary": "Binds the tenant's smart account, Tron permission, or Solana guarded authority after the operator-led setup flow.",
          "notes": [
            "Ethereum uses a Safe or simple account with a bounded guard; Tron uses native Active Permission; Solana uses a program-owned vault authority when configured. No private key transfer is part of this flow."
          ],
          "response_example": "{ \"success\": true, \"sessionKeyBindingVerified\": true }"
        },
        {
          "method": "GET",
          "path": "/admin/audit-log",
          "title": "Read the audit log",
          "auth": "admin",
          "operatorOnly": true,
          "summary": "Returns the immutable record of admin and tenant writes: tenant, wallet and API-key creation or revocation, status and limit changes with before/after values, kill-switch use, and the refund lifecycle.",
          "notes": [
            "Optional tenant_id, target_type, target_id and limit filters. tenant_id matches rows where the tenant is the actor or the target.",
            "Raw API keys never appear in an audit entry — only the key id and prefix. Admin actions carry no per-user id because operators share one key; the IP address is the only actor signal."
          ],
          "response_example": "{\n  \"object\": \"list\",\n  \"data\": [{ \"id\": \"aud_...\", \"action\": \"tenant.limits_updated\", \"target_type\": \"tenant\", \"target_id\": \"tn_...\", \"actor\": { \"type\": \"admin\", \"ip\": \"203.0.113.10\" }, \"before\": { \"dailyPaymentCountCap\": null }, \"after\": { \"dailyPaymentCountCap\": 2000 }, \"created_at\": \"...\" }]\n}"
        }
      ]
    }
  ],
  "roadmap_not_built": [
    {
      "title": "Payout links",
      "description": "Payouts require a known recipient wallet; there is no claim flow."
    },
    {
      "title": "Fiat settlement and bank payout",
      "description": "Settlement is always a ledger credit against a non-custodial transfer. There is no auto-converted fiat balance and no external bank payout leg."
    },
    {
      "title": "Recurring billing",
      "description": "Each checkout payment is single-use and requires a fresh wallet signature."
    },
    {
      "title": "Beneficiaries and KYC",
      "description": "Recipients are raw wallet addresses, not compliance contacts."
    },
    {
      "title": "Per-payment void requests",
      "description": "Cancellation is currently job-level and pre-execution only."
    },
    {
      "title": "Deposit detection",
      "description": "Payment Channel addresses are generated, but no chain watcher links deposits to customers or the ledger."
    },
    {
      "title": "Refunds beyond Ethereum checkout sessions",
      "description": "Refunds exist only for a checkout-session-originated payment on Ethereum; the standalone payment-request flow has nothing to attach one to."
    },
    {
      "title": "Hosted public documentation",
      "description": "Both OpenAPI specs are served live via a Redoc-rendered page on the relay itself; assigning a dedicated public docs domain is still an operator decision."
    },
    {
      "title": "Beneficiary and batch objects",
      "description": "There is no Beneficiary/payout-destination CRUD resource and no first-class batch object for bulk disbursement; recipients and batches exist only inside a request payload."
    },
    {
      "title": "Supported countries and industries",
      "description": "No geographic or industry eligibility API is published."
    }
  ],
  "resources": [
    {
      "title": "Documentation hub",
      "url": "https://f-pay.com/docs",
      "format": "text/html"
    },
    {
      "title": "Complete written reference",
      "url": "https://f-pay.com/api",
      "format": "text/html"
    },
    {
      "title": "Machine-readable catalogue",
      "url": "https://f-pay.com/docs.json",
      "format": "application/json"
    },
    {
      "title": "Relay OpenAPI specification",
      "url": "https://xwefglhexusimsmdgvtd.supabase.co/functions/v1/openapi-spec",
      "format": "application/vnd.oai.openapi+json"
    },
    {
      "title": "Static HTML reference (no JavaScript required)",
      "url": "https://f-pay.com/api-reference.html",
      "format": "text/html"
    },
    {
      "title": "Markdown reference",
      "url": "https://f-pay.com/api-reference.md",
      "format": "text/markdown"
    },
    {
      "title": "Complete LLM reference",
      "url": "https://f-pay.com/llms-full.txt",
      "format": "text/plain"
    },
    {
      "title": "Relay architecture",
      "url": "https://f-pay.com/docs/relay-gateway",
      "format": "text/html"
    },
    {
      "title": "Disbursement engine",
      "url": "https://f-pay.com/docs/disbursement-engine",
      "format": "text/html"
    },
    {
      "title": "Webhook tester",
      "url": "https://f-pay.com/docs/webhook-tester",
      "format": "text/html"
    },
    {
      "title": "Webhook delivery log",
      "url": "https://f-pay.com/docs/webhooks",
      "format": "text/html"
    }
  ]
}
