{
  "openapi": "3.1.0",
  "info": {
    "title": "TokenPay Merchant API",
    "version": "1.0.0",
    "summary": "The merchant-facing HTTP surface for TokenPay payments, settlements, and webhook management.",
    "description": "This document is the machine-readable contract for every v1 merchant-facing\nendpoint served by `gateway-service`. It is the source of truth for the\ndeveloper portal, SDKs, and Postman collection — if the spec and the\nimplementation disagree, the implementation is the bug.\n\nScope boundary:\n  * **Included**: the 20 routes under `/v1` that are part of the public\n    merchant contract.\n  * **Not included**: admin-only routes (`/v1/merchants/*`,\n    `/admin/*`), dashboard dual-auth routes (`/v1/merchant-payments`,\n    `/v1/merchant-settlements`, `/v1/merchant-webhooks`,\n    `/v1/merchants/me/*`), service-to-service endpoints (`/internal/*`),\n    and merchant-auth/onboarding/application surfaces\n    (`/merchant-auth/*`, `/merchant-onboarding/*`,\n    `/merchant-applications/*`).\n\nSee `docs/api/VERSIONING.md` for the stability guarantee and deprecation\npolicy that applies to every operation below.\n",
    "contact": {
      "name": "TokenPay Developer Support",
      "email": "developers@tokenpayment.io",
      "url": "https://tokenpayment.io/developers"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://tokenpayment.io/legal/api-terms"
    }
  },
  "servers": [
    {
      "url": "https://api.tokenpayment.io",
      "description": "Production (public endpoint — see BUG-162 for DNS stand-up status)."
    },
    {
      "url": "https://54.251.37.245",
      "description": "Production EC2 Elastic IP (ap-southeast-1, pre-DNS fallback)."
    }
  ],
  "tags": [
    {
      "name": "Payments",
      "description": "Create, inspect, and mutate gateway payments."
    },
    {
      "name": "Rates",
      "description": "Liquidity and pricing snapshots for supported currencies."
    },
    {
      "name": "Reference",
      "description": "Read-only reference data (e.g. Thai bank directory)."
    },
    {
      "name": "PromptPay",
      "description": "PromptPay (Thailand) EMVCo QR helpers."
    },
    {
      "name": "Settlements",
      "description": "Merchant payout batches, pending accruals, reports, exports, disputes, adjustments, audit log, retry."
    },
    {
      "name": "Webhooks",
      "description": "Webhook delivery history for the authenticated merchant."
    },
    {
      "name": "FX",
      "description": "Foreign-exchange reference prices (read) and quote locks (write). Served by the `fx-service`\non port :4003 (moved from :3011 in S099 BUG-192 — settlement-service collision), scraped by\nPrometheus directly (Rule 26 — api-gateway does **not** proxy `/metrics` or `/fx/*`).\nIntroduced in Session 098.\n"
    },
    {
      "name": "Merchants",
      "description": "Merchant-scoped read endpoints (balance, receive-capacity, marketplace role).\nAuthenticated via MerchantJWTAuth + MerchantDashboardAuth dual-path (Rule 22).\nIntroduced incrementally across S100 (routing) and S101 (deposit model). Tag\ndeclared in S102(b) WS9 fold-in to align with operation-level tag usage.\n"
    },
    {
      "name": "PendingFunds",
      "description": "Pending-funds lifecycle: create on top-up intent, validate via the configured\nReceiptValidator (NullValidator in AU per Rule 22, EasySlipValidator in TH),\nand confirm via receiver-side proof (`/v1/pending-funds/{id}/receiver-confirm`,\nS102). Status terminals: `confirmed` (verified → ledger mint), `failed`\n(validator denial or receiver-confirm window expiry per S102(b) Gate G.1.A),\n`expired` (TTL-elapsed before any receiver action). Tag declared in S102(b)\nWS9 fold-in.\n"
    },
    {
      "name": "SellOrders",
      "description": "Outbound payouts / sell-order lifecycle (S103(a) PR #130). Merchant or end-user\ninitiates a sell order against PAY held in their available account; the gateway\ndrives the lifecycle `pending → matched → cash_leg_in_flight → completed` (or\n`→ failed` on `expired_unmatched`, `cash_leg_retry_exhausted`, or\n`cancelled_by_merchant`). The `disputed` status value is intentionally absent in\nS103(a); it is added in S103(b) via `ALTER TYPE sell_order_status ADD VALUE`.\n\n**Posture (LAW)**: the \"cash leg\" is the merchant's external rail (their bank,\ntheir PSP); TokenPay records `cash_leg_attempts` + `last_cash_leg_attempt_at` and\nburns PAY on completion via the existing ledger-service HTTP client. No payment\nprocessor wired, no fiat balance held.\n\n**Match-layer integrity** (ADR-103a-3): `sell_orders.matched_buy_order_id` is\nplain `UUID NULL` in S103(a) (the `buy_orders` table doesn't ship until S103(b)).\nThe FK constraint is added in mig 061 once `buy_orders` is live.\n\n**No dispute creation** (ADR-103a-2) in S103(a). Cash-leg failure terminal flips\n`sell_orders.status → failed` with `failure_reason='cash_leg_retry_exhausted'`\nand fires `sell_order.failed` webhook only. S103(b) adds `gateway.dispute.opened`\nalongside (additive, not replacing).\n\nIncludes admin read-only views over `receiver_confirm_audit` (S102 mig 059)\nunder `/admin/audit/receiver-confirm*` — these reuse the existing audit table\nwith no new migration; they are grouped under SellOrders because they ship in\nthe same PR. Tag declared in S103(a) WS9.\n"
    },
    {
      "name": "Gateway Disputes",
      "description": "TokenPay dispute lifecycle (S103(b) Phase 1). A `dispute` row tracks an\nadversarial event against a `sell_order`: cash-leg failure (auto-opened\nin S103(b) Phase 2A), merchant-initiated complaint, or chargeback. The\ngateway drives the lifecycle `opened → investigating → (resolved |\nescalated)`; resolution is super-admin-only and may emit a ledger\n`mint` (favor merchant), `burn` (favor counterparty), or no-op\ndepending on the outcome + originating sell_order kind.\n\n**Posture (LAW)**: dispute resolution moves PAY through ledger-service\n(mint/burn) only — NEVER fiat. The `cash_leg_failure` kind originates\nfrom a sell-order's external cash leg failing on the merchant's rail;\nTokenPay records the dispute and (in Phase 2A) auto-opens it, but the\nfiat reconciliation happens off-platform.\n\n**Auth model** (ADR-103b-1 Q4): merchant-facing endpoints use\nMerchantJWTAuth + MerchantDashboardAuth dual-path (Rule 22). Admin\nendpoints use `InternalServiceAuth` token — super-admin enforcement\nis upstream at api-gateway (mirrors S103(a) `/admin/audit/receiver-confirm`).\nThe `gateway_dispute_events.actor_user_id` column captures the resolving\nuser-id from the upstream-validated header.\n\n**Phase split** (ADR-103b-1):\n- **Phase 1 (this PR)** — schema (mig 061) + 7 routes + 3 webhook\n  events (`gateway.dispute.opened`, `gateway.dispute.resolved`, `gateway.dispute.escalated`).\n- **Phase 2A** — wires cash-leg sweeper callback to\n  `OpenGatewayCashLegFailureDispute` under `GATEWAY_DISPUTE_AUTO_OPEN_ENABLED`.\n- **Phase 2B** — admin-web + merchant-dashboard UIs.\n- **Phase 2C** — audit-archival worker + `buy_orders` write paths +\n  `sell_orders.matched_buy_order_id` FK addition.\n\n**Webhook contract** (ADR-103b-1 Q3): `gateway.dispute.opened` fires alongside\nthe existing S103(a) `sell_order.failed` (additive, NOT replacing).\nMerchants integrated against S103(a) sell-order webhooks see no\nbreaking change. Tag declared in S103(b) Phase 1.\n"
    },
    {
      "name": "BuyOrders",
      "description": "TokenPay merchant buy-order surface (S103(b) Phase 2C). A `buy_order`\nis the merchant-side counterparty to an existing `sell_order`: the\nmerchant locks fiat against PAY they want to acquire, and the buy-side\nmatcher pairs pending sells ⇔ buys FIFO inside one atomic TX +\nledger-service `CreateTransaction`. Mint-on-resolution: a successful\nmatch debits `MintSourceAccountID` and credits the buyer-merchant's PAY\navailable account. There is NO `/confirm-cash-leg` verb on buy-side —\nthe cash leg is the merchant's funding leg and is settled at match\ntime inside the matcher's atomic TX (Phase 2C §Q3 lock).\n\n**Posture (LAW)**: buy-order resolution moves PAY through\nledger-service only — NEVER fiat. The matching engine fails-closed\nwhen `MintSourceAccountID` is empty (Rule 33).\n\n**Auth model** (ADR-103b-1 Q4): merchant-facing endpoints use\nMerchantJWTAuth + MerchantDashboardAuth dual-path (Rule 22), same\nstack as `/v1/merchant-sell-orders`. POST verbs go through the\nshared Idempotency middleware (Rule G.3.A).\n\n**Webhook contract**: `buy_order.created`, `buy_order.matched`, and\n`buy_order.cancelled` fire alongside the corresponding sell-side\nlifecycle events. Tag declared in S103(b) Phase 2C.\n"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/v1/payments": {
      "post": {
        "tags": [
          "Payments"
        ],
        "operationId": "createPayment",
        "deprecated": true,
        "x-sunset": "2026-07-24T00:00:00Z",
        "x-replacement-path": "/v1/trades",
        "summary": "Create a gateway payment. [DEPRECATED — use `/v1/trades`]",
        "security": [
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePaymentRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Payment created and matched (or matching).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentWithMatchesEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Validation error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate `order_id`, or insufficient liquidity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Payments"
        ],
        "operationId": "listPayments",
        "summary": "List payments for the authenticated merchant.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentStatusFilter"
          },
          {
            "$ref": "#/components/parameters/FromDate"
          },
          {
            "$ref": "#/components/parameters/ToDate"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of payments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentListEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payments/{id}": {
      "get": {
        "tags": [
          "Payments"
        ],
        "operationId": "getPayment",
        "summary": "Get a payment by ID.",
        "description": "If the payment is in `matched` or `payment_pending` state and the\n`expires_at` deadline has passed, this endpoint lazily transitions\nthe payment to `expired` (and fires a `payment.expired` webhook).\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentID"
          }
        ],
        "responses": {
          "200": {
            "description": "Payment details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Invalid ID format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Payment not found, or not owned by this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payments/{id}/checkout": {
      "get": {
        "tags": [
          "Payments"
        ],
        "operationId": "getPaymentCheckout",
        "summary": "Fetch the data needed to render the checkout widget.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentID"
          }
        ],
        "responses": {
          "200": {
            "description": "Checkout view-model.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Invalid ID format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Payment not found, or not owned by this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payments/{id}/cancel": {
      "post": {
        "tags": [
          "Payments"
        ],
        "operationId": "cancelPayment",
        "summary": "Cancel a payment that is still in an early (pre-paid) state.",
        "description": "Cancellation is only permitted when the payment is in `created` or\n`matching` status — see `PaymentStatus.CanCancel()`.\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentID"
          }
        ],
        "responses": {
          "200": {
            "description": "Payment cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Invalid ID format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Payment not found, or not owned by this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Payment is past the cancellable window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payments/{id}/proof": {
      "post": {
        "tags": [
          "Payments"
        ],
        "operationId": "uploadPaymentProof",
        "summary": "Upload a payment proof image and transition to `confirming`.",
        "description": "Accepts a multipart form with a single file field named `proof`. The\npayment must be in `matched` or `payment_pending` state for the\nupload to be accepted.\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentID"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "proof"
                ],
                "properties": {
                  "proof": {
                    "type": "string",
                    "format": "binary",
                    "description": "The payment proof image (any common raster format)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Proof accepted; payment is now `confirming`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProofUploadEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Invalid ID format, or missing `proof` field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Payment not found, or not owned by this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Payment is not in a state that accepts proof.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Internal error while reading the proof file.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payments/{id}/promptpay-qr": {
      "get": {
        "tags": [
          "Payments",
          "PromptPay"
        ],
        "operationId": "getPaymentPromptPayQR",
        "summary": "PNG render of the dynamic PromptPay QR for a payment.",
        "description": "Public endpoint — no `X-API-Key` required. Access control is by\nunguessable UUID, matching the seller-facing checkout page. Only\navailable for THB payments; returns `NOT_TH` otherwise.\n",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentID"
          }
        ],
        "responses": {
          "200": {
            "description": "PNG image of the EMVCo PromptPay QR.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "`public, max-age=300`."
              }
            },
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Invalid payment ID, or payment is not THB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Handler was not configured with a DB pool, or an internal render error occurred.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/rates": {
      "get": {
        "tags": [
          "Rates"
        ],
        "operationId": "getRates",
        "summary": "Snapshot of liquidity and best-rate for a currency/payment-method pair.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "currency",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "AUD",
                "USD",
                "THB",
                "SGD"
              ]
            }
          },
          {
            "in": "query",
            "name": "payment_method",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "E.g. `payid`, `promptpay`, `bank_transfer`."
          }
        ],
        "responses": {
          "200": {
            "description": "Rate snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RatesEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Missing required query parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Matching service unreachable or returned an error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/reference/thai-banks": {
      "get": {
        "tags": [
          "Reference"
        ],
        "operationId": "listThaiBanks",
        "summary": "Directory of Thai banks supported as settlement destinations.",
        "description": "Public — no authentication required. Response is cached server-side\nfor 1 hour; clients get `Cache-Control: public, max-age=3600`.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "Bank directory.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ThaiBanksEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Database unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/promptpay/qr": {
      "post": {
        "tags": [
          "PromptPay"
        ],
        "operationId": "generatePromptPayQR",
        "summary": "Generate an EMVCo PromptPay payload string from a PromptPay ID + amount.",
        "description": "Public endpoint. Returns the EMVCo payload string; the caller (widget\nor mobile) renders the QR client-side.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PromptPayGenerateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Payload generated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PromptPayGenerateEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Validation error (missing ID, negative amount, malformed ID).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/settlements": {
      "get": {
        "tags": [
          "Settlements"
        ],
        "operationId": "listSettlements",
        "summary": "List settlements (cursor-paginated) for the authenticated merchant.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SettlementStatusFilter"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated settlements.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementListEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/merchants/{id}/balance": {
      "get": {
        "tags": [
          "Merchants"
        ],
        "operationId": "getMerchantBalance",
        "summary": "Merchant PAY balance + deficit projection.",
        "description": "Returns the merchant's available PAY balance, buffer cushion, net\noutbound obligation (sum of validated-pending receiver obligations),\nand projected deficit in micro-PAY along with prospective penalty\nAPR (bps), entry fee (USD-quoted, settled in PAY), and countdown\ndays. Backed by `merchant_deficit_v` (mig 058).\n\n**Scoping**: the path `id` MUST equal the merchant the caller is\nscoped to via `MerchantJWTAuth` or `MerchantDashboardAuth`.\nCross-merchant reads return `403 FORBIDDEN`.\n\nVisible to merchant: `available_micro_pay`, `buffer_micro_pay`,\n`net_obligation_micro_pay`, `deficit_micro_pay`, `apr_bps`,\n`entry_fee_usd`, `countdown_days`. Admin-only fields\n(`buffer_threshold_micro_pay`, `buffer_last_recalc_at`,\n`trailing_30d_volume_micro_pay`) are NOT projected here — see\n`/admin/deficit/{merchantId}` for those.\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Merchant ID (UUID v4). MUST equal the caller's scope."
          }
        ],
        "responses": {
          "200": {
            "description": "Balance snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/x-fx-service-schemas/MerchantBalanceEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid merchant credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Cross-merchant read attempt (path id != caller scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Merchant has no PAY balance row yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/pending-funds/{id}/receiver-confirm": {
      "post": {
        "tags": [
          "PendingFunds"
        ],
        "operationId": "receiverConfirmPendingFunds",
        "summary": "Receiver-side proof on a pending_funds row.",
        "description": "S102 (PR #125). The receiver of a pending_funds row that previously\npassed buyer-side validation (status =\n`validated_pending_receiver_confirm`) submits their own receipt\nevidence; the gateway runs it through the configured\nReceiptValidator (EasySlip in TH; Null in AU per Rule 22) and either:\n\n  * `verified` — mints PAY into the receiver's available account via\n    ledger-service (idempotency key = pending_funds.id) and flips\n    `pending_funds.status → confirmed`.\n  * `failed` — flips `status → failed` with `failure_reason` derived\n    from the validator's `FailureCode`.\n  * `unverified` / `timeout` — row stays in\n    `validated_pending_receiver_confirm`, attempt counter bumps.\n    After `RECEIVER_CONFIRM_MAX_ATTEMPTS` non-verified, non-timeout\n    attempts the row is auto-failed with\n    `failure_reason='max_attempts_exceeded'`.\n  * `malformed` — caller-side rejection (bad base64, unsupported\n    content_type); 400 returned without invoking the validator.\n\n**Posture (LAW)**: TokenPay never touches fiat. The validator's\nOCR amount is persisted to `receiver_confirm_audit.validator_response`\nfor audit trail only and never crosses the ledger boundary. The PAY\nmint amount on the verified path is derived from\n`pending_funds.amount_minor`, not from the validator response.\n\n**Append-only audit**: every call that reaches the validator (i.e.\noutcome ∈ verified|failed|unverified|timeout, but NOT malformed) writes\nexactly one row to `receiver_confirm_audit` (mig 059) within the same\nDB transaction as the `pending_funds` status update. The audit table\nhas a database-level trigger rejecting UPDATE / DELETE / TRUNCATE\nfrom application code (ADR-102-2).\n\n**Caller**: must be the receiver of the row. Buyer / third-party\ncallers receive `403 PENDING_FUNDS_NOT_RECEIVER`.\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "pending_funds row ID."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReceiverConfirmRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Audit row inserted. Inspect `outcome` to determine resolution\n(the HTTP code is 201 even on validator-fail / unverified, since\nthe call did record an audit attempt).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReceiverConfirmResult"
                }
              }
            }
          },
          "400": {
            "description": "Body invalid, content_type unsupported\n(`RECEIVER_CONFIRM_CONTENT_TYPE_UNSUPPORTED`), or receipt_base64\nmalformed (`RECEIVER_CONFIRM_MALFORMED_RECEIPT`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Caller is not the receiver (`PENDING_FUNDS_NOT_RECEIVER`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "pending_funds row not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Row is terminal (`PENDING_FUNDS_TERMINAL`) or not in status\n`validated_pending_receiver_confirm`\n(`PENDING_FUNDS_VALIDATION_UNVERIFIED`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "502": {
            "description": "Verified outcome but ledger-service is unavailable\n(`PENDING_FUNDS_LEDGER_UNAVAILABLE`). Tx rolled back; no audit\nrow written; receiver may retry without consuming a slot.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/merchant-sell-orders": {
      "get": {
        "tags": [
          "SellOrders"
        ],
        "operationId": "listMerchantSellOrders",
        "summary": "List sell orders for the authenticated merchant.",
        "description": "Cursor-paginated list of sell orders owned by the authenticated merchant.\nFilterable by `status` (multi-select), `currency`, `target_currency`, and\ncreation-window (`created_after`, `created_before`).\n\nDual-path auth: MerchantJWTAuth (programmatic) + MerchantDashboardAuth\n(browser session) per Rule 22 (S103(a)).\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "pending",
                  "matched",
                  "cash_leg_in_flight",
                  "completed",
                  "failed"
                ]
              }
            },
            "style": "form",
            "explode": false,
            "description": "Filter by status (comma-separated for multi-select)."
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor returned by the previous page."
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of sell orders.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SellOrderListResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "SellOrders"
        ],
        "operationId": "createMerchantSellOrder",
        "summary": "Create a new sell order.",
        "description": "Creates a sell order against PAY held in the merchant's available\naccount. The handler sets `expires_at = NOW() + SELL_ORDER_PENDING_TTL_HOURS`\n(per ADR-103a-1 Q2; sweeper does not infer the deadline).\n\n**Idempotency**: required `Idempotency-Key` header (UUIDv4 recommended).\nRepeated calls with the same key + same body return the same sell_order\nrow; same key + different body returns 409 `IDEMPOTENCY_KEY_CONFLICT`.\n\n**Posture (LAW)**: the `target_fiat_amount` + `target_currency` are\nrecorded for audit and merchant-facing display only; TokenPay does not\ninitiate a fiat transfer. The merchant's external rail executes the cash\nleg; the merchant calls `confirm-cash-leg` when the rail completes.\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SellOrderCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Sell order created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SellOrder"
                }
              }
            }
          },
          "400": {
            "description": "Body invalid (missing fields, bad currency pair, amount ≤ 0).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency-Key conflict (`IDEMPOTENCY_KEY_CONFLICT`) or insufficient\navailable PAY balance (`SELL_ORDER_INSUFFICIENT_BALANCE`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/merchant-sell-orders/{id}": {
      "get": {
        "tags": [
          "SellOrders"
        ],
        "operationId": "getMerchantSellOrder",
        "summary": "Read a single sell order.",
        "description": "Returns the sell order for the given UUID. The caller must be the\nowning merchant; cross-merchant reads return 403\n`SELL_ORDER_NOT_OWNER`.\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sell order detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SellOrder"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Caller is not the owner (`SELL_ORDER_NOT_OWNER`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Sell order not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/merchant-sell-orders/{id}/confirm-cash-leg": {
      "post": {
        "tags": [
          "SellOrders"
        ],
        "operationId": "confirmSellOrderCashLeg",
        "summary": "Confirm the merchant has executed the cash leg externally.",
        "description": "Merchant signal that they have executed the fiat cash leg via their\nexternal rail. The handler:\n  1. Asserts the sell_order is in `cash_leg_in_flight` (else 409).\n  2. Increments `cash_leg_attempts` and updates `last_cash_leg_attempt_at`.\n  3. Validates external receipt evidence in the request body (handler\n     persists for audit; no validator integration in S103(a)).\n  4. On success: flips status → `completed`, fires `sell_order.completed`\n     webhook, and burns PAY via ledger-service (idempotency key =\n     `sell_order.id`).\n\n**Idempotency**: required `Idempotency-Key` header. Repeated calls with\nthe same key are no-ops.\n\n**Retry policy**: if cash leg fails (rail rejects, OCR mismatch, etc.),\nmerchant calls again with new `Idempotency-Key`. The sweeper\n(`SweepCashLegRetryExhausted`) flips to `failed` after\n`CASH_LEG_RETRY_MAX` attempts past `CASH_LEG_RETRY_TIMEOUT_HOURS`.\nADR-103a-2: no dispute auto-creation in S103(a).\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SellOrderCashLegConfirmRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cash leg recorded. Inspect response `outcome` to determine resolution\n(`completed`, `cash_leg_in_flight` for retry-pending, or `failed` if\nsweeper has already flipped).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SellOrder"
                }
              }
            }
          },
          "400": {
            "description": "Body invalid (missing receipt evidence, malformed base64).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Caller is not the owner (`SELL_ORDER_NOT_OWNER`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Sell order not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Sell order not in `cash_leg_in_flight` (`SELL_ORDER_BAD_STATE`) or\nIdempotency-Key conflict (`IDEMPOTENCY_KEY_CONFLICT`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "502": {
            "description": "Burn failed because ledger-service is unavailable\n(`SELL_ORDER_LEDGER_UNAVAILABLE`). Tx rolled back; merchant may\nretry without consuming a slot.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/admin/audit/receiver-confirm": {
      "get": {
        "tags": [
          "SellOrders"
        ],
        "operationId": "listReceiverConfirmAudit",
        "summary": "Admin read-only list of receiver-confirm audit rows.",
        "description": "Super-admin only. Cursor-paginated list of `receiver_confirm_audit`\nrows (S102 mig 059 — no new table in S103(a)). Filterable by `outcome`\n(multi-select: `verified` / `failed` / `unverified` / `timeout`),\n`pending_funds_id`, and `validator_kind` (`null` / `easyslip`).\n\n**Append-only invariant** (ADR-102-2): this endpoint is read-only;\nthe gateway has no UPDATE / DELETE path on `receiver_confirm_audit`.\nS3 cold archival of rows older than 90 days ships in S103(b) per\nADR-102-2 retention.\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "outcome",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "verified",
                  "failed",
                  "unverified",
                  "timeout"
                ]
              }
            },
            "style": "form",
            "explode": false
          },
          {
            "in": "query",
            "name": "pending_funds_id",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "validator_kind",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "null_validator",
                "easyslip"
              ]
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of audit rows.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReceiverConfirmAuditListResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Caller is not super-admin (`ADMIN_FORBIDDEN`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/admin/audit/receiver-confirm/{id}": {
      "get": {
        "tags": [
          "SellOrders"
        ],
        "operationId": "getReceiverConfirmAuditRow",
        "summary": "Admin read-only detail for a single audit row.",
        "description": "Super-admin only. Returns the full audit row including the JSONB\n`validator_response` for diff inspection. Read-only; no mutation\npath exists in S103(a).\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Audit row detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReceiverConfirmAuditRow"
                }
              }
            }
          },
          "401": {
            "description": "Missing caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Caller is not super-admin (`ADMIN_FORBIDDEN`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Audit row not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/merchant-disputes": {
      "get": {
        "tags": [
          "Gateway Disputes"
        ],
        "operationId": "listMerchantDisputes",
        "summary": "List disputes for the authenticated merchant.",
        "description": "Cursor-paginated list of disputes scoped to the authenticated\nmerchant via the `sell_orders.merchant_id` join. Filterable by\n`status` (multi-select) and `kind` (multi-select).\n\nDual-path auth: MerchantJWTAuth (programmatic) + MerchantDashboardAuth\n(browser session) per Rule 22.\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "opened",
                  "investigating",
                  "resolved",
                  "escalated"
                ]
              }
            },
            "style": "form",
            "explode": false
          },
          {
            "in": "query",
            "name": "kind",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "cash_leg_failure",
                  "merchant_dispute",
                  "chargeback"
                ]
              }
            },
            "style": "form",
            "explode": false
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of disputes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GatewayDisputeListResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Gateway Disputes"
        ],
        "operationId": "createMerchantDispute",
        "summary": "Create a merchant-initiated dispute.",
        "description": "Creates a `dispute` row with `kind='merchant_dispute'`, `status='opened'`\nagainst the referenced `sell_order_id`. The handler asserts the caller\nowns the referenced sell_order (cross-merchant returns 403\n`GATEWAY_DISPUTE_CROSS_MERCHANT`). Emits `gateway.dispute.opened` webhook.\n\n**Idempotency**: required `Idempotency-Key` header (UUIDv4 recommended).\nRepeated calls with the same key + same body return the same dispute\nrow; same key + different body returns 409 `IDEMPOTENCY_KEY_CONFLICT`.\n\n**Posture (LAW)**: dispute creation persists an audit row and emits a\nwebhook only — no fiat or PAY ledger op fires at open time. Ledger\nops are deferred to admin resolution.\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GatewayDisputeOpenRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Gateway dispute created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GatewayDispute"
                }
              }
            }
          },
          "400": {
            "description": "Body invalid (missing `sell_order_id`, malformed UUID, notes empty or > 4000 chars).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Caller is not the owner of the referenced sell_order (`GATEWAY_DISPUTE_CROSS_MERCHANT`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Referenced sell_order not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency-Key conflict (`IDEMPOTENCY_KEY_CONFLICT`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/merchant-disputes/{id}": {
      "get": {
        "tags": [
          "Gateway Disputes"
        ],
        "operationId": "getMerchantDispute",
        "summary": "Read a single dispute (merchant-scoped).",
        "description": "Returns the dispute for the given UUID. The caller must own the\nunderlying sell_order; cross-merchant reads return 403\n`GATEWAY_DISPUTE_CROSS_MERCHANT`. Merchant-facing detail does NOT include\nthe `gateway_dispute_events` array (admin-only).\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Gateway dispute detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GatewayDispute"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Caller is not the owner (`GATEWAY_DISPUTE_CROSS_MERCHANT`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Gateway dispute not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/admin/disputes": {
      "get": {
        "tags": [
          "Gateway Disputes"
        ],
        "operationId": "listAdminDisputes",
        "summary": "Admin list of disputes across all merchants.",
        "description": "Super-admin-enforced (upstream at api-gateway via `InternalServiceAuth`\ntoken + admin-web super-admin guard middleware, per ADR-103b-1 Q4).\nCursor-paginated list of `disputes` rows. Filterable by `status`\n(multi-select), `kind` (multi-select), `sell_order_id`, and\nopened-window (`opened_after`, `opened_before`).\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "opened",
                  "investigating",
                  "resolved",
                  "escalated"
                ]
              }
            },
            "style": "form",
            "explode": false
          },
          {
            "in": "query",
            "name": "kind",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "cash_leg_failure",
                  "merchant_dispute",
                  "chargeback"
                ]
              }
            },
            "style": "form",
            "explode": false
          },
          {
            "in": "query",
            "name": "sell_order_id",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "opened_after",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "in": "query",
            "name": "opened_before",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of disputes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GatewayDisputeAdminListResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Caller is not super-admin (`ADMIN_FORBIDDEN`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/admin/disputes/queue": {
      "get": {
        "tags": [
          "Gateway Disputes"
        ],
        "operationId": "getAdminDisputeQueue",
        "summary": "Stuck-PAY admin queue (non-terminal disputes ordered oldest-first).",
        "description": "Super-admin-enforced. Returns the operational queue of disputes that\nrequire admin attention: all rows with `status IN ('opened',\n'investigating')` ordered by `opened_at ASC` (oldest first). Used\nby the Phase 2B admin-web `/admin/disputes` UI as the default view.\nUnlike `/admin/disputes` this endpoint is NOT cursor-paginated — it\nreturns the full non-terminal set (capped server-side; expected to\nbe small in steady state).\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Non-terminal disputes (oldest-first).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GatewayDisputeQueueResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Caller is not super-admin (`ADMIN_FORBIDDEN`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/admin/disputes/{id}": {
      "get": {
        "tags": [
          "Gateway Disputes"
        ],
        "operationId": "getAdminDispute",
        "summary": "Admin read of a single dispute (with full event audit trail).",
        "description": "Super-admin-enforced. Returns the dispute row plus the full\n`gateway_dispute_events` audit array (every state transition, with\n`event_kind`, `actor_user_id`, `details JSONB`, `recorded_at`).\nMerchant-facing `/v1/merchant-disputes/{id}` does NOT return the\nevents array; this admin endpoint is the only path that exposes it.\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Gateway dispute detail with events.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GatewayDisputeDetailResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Caller is not super-admin (`ADMIN_FORBIDDEN`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Gateway dispute not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/admin/disputes/{id}/resolve": {
      "post": {
        "tags": [
          "Gateway Disputes"
        ],
        "operationId": "resolveAdminDispute",
        "summary": "Super-admin resolve a dispute (terminal transition).",
        "description": "Super-admin-enforced. Maps the supplied `outcome` to:\n  - `merchant_favored`     → status `resolved`; on `cash_leg_failure`\n    disputes, mints PAY to the merchant via ledger-service\n    (idempotency key = `dispute.id`).\n  - `counterparty_favored` → status `resolved`; on `merchant_dispute`\n    kinds, burns PAY from the merchant.\n  - `no_action`            → status `resolved`; no ledger op.\n  - `escalated`            → status `escalated`; no ledger op.\nOn any non-`no_action` / non-`escalated` outcome the service derives\nthe ledger op from the originating sell_order kind. Single tx covers\nthe dispute UPDATE + `gateway_dispute_events` INSERT + ledger call (rolls\nback atomically on ledger failure).\n\n**Idempotency**: required `Idempotency-Key` header. Repeated calls\nwith the same key are no-ops.\n\n**Auth (Q4)**: super-admin via api-gateway upstream + `InternalServiceAuth`\ntoken; gateway-service does not re-validate the role. The\n`gateway_dispute_events.actor_user_id` column captures the user from the\nupstream-validated header.\n\n**Posture (LAW)**: ledger ops mint or burn PAY only; no fiat. Resolve\non a terminal dispute returns 409 `GATEWAY_DISPUTE_ALREADY_RESOLVED`.\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GatewayDisputeResolveRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Gateway dispute resolved (terminal status set).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GatewayDispute"
                }
              }
            }
          },
          "400": {
            "description": "Body invalid (missing/invalid `outcome`, notes empty or > 4000 chars).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing caller identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Caller is not super-admin (`ADMIN_FORBIDDEN`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Gateway dispute not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Gateway dispute already in a terminal state (`GATEWAY_DISPUTE_ALREADY_RESOLVED`)\nor Idempotency-Key conflict (`IDEMPOTENCY_KEY_CONFLICT`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Ledger op failed. On `merchant_favored` of a `cash_leg_failure`\ndispute when `MINT_SOURCE_ACCOUNT_ID` is unconfigured, returns\n`GATEWAY_DISPUTE_LEDGER_MINT_SOURCE_MISSING` (transaction rolls back).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/merchant-buy-orders": {
      "get": {
        "tags": [
          "BuyOrders"
        ],
        "operationId": "listMerchantBuyOrders",
        "summary": "List buy_orders for the calling merchant",
        "description": "Cursor-paginated list of `buy_order` rows scoped to the calling\nmerchant. Mirrors `GET /v1/merchant-sell-orders` shape. Auth:\nMerchantJWTAuth + MerchantDashboardAuth.\n",
        "security": [
          {
            "merchantJWT": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "matched",
                "completed",
                "failed",
                "cancelled"
              ]
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Buy-order page (rows + next_cursor).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid merchant JWT."
          },
          "403": {
            "description": "JWT does not authorise the requested merchant_id."
          }
        }
      },
      "post": {
        "tags": [
          "BuyOrders"
        ],
        "operationId": "createMerchantBuyOrder",
        "summary": "Create a buy_order in pending status",
        "description": "Creates a `buy_order` row in `pending` status. Idempotency-Key\nheader REQUIRED (Rule G.3.A) — duplicate keys within the 24h\nwindow return the original 2xx response. The matcher pairs the\nnew row against pending sells on its next tick (gated by\n`BUY_ORDER_MATCHER_ENABLED`).\n",
        "security": [
          {
            "merchantJWT": []
          }
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — returns the new buy_order row.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Validation error (invalid amount, currency, cash_leg_source)."
          },
          "401": {
            "description": "Missing or invalid merchant JWT."
          },
          "409": {
            "description": "Idempotency-Key replay collision with a different request body."
          }
        }
      }
    },
    "/v1/merchant-buy-orders/{id}": {
      "get": {
        "tags": [
          "BuyOrders"
        ],
        "operationId": "getMerchantBuyOrder",
        "summary": "Fetch a single buy_order by id",
        "security": [
          {
            "merchantJWT": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Buy-order row.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "403": {
            "description": "Buy-order belongs to another merchant."
          },
          "404": {
            "description": "Buy-order does not exist."
          }
        }
      }
    },
    "/v1/merchant-buy-orders/{id}/cancel": {
      "post": {
        "tags": [
          "BuyOrders"
        ],
        "operationId": "cancelMerchantBuyOrder",
        "summary": "Cancel a pending buy_order",
        "description": "Transitions a buy_order from `pending` to `cancelled`. Only\ncancellable while in `pending`; matched/completed/failed rows\nreturn 409. Idempotency-Key REQUIRED (Rule G.3.A).\n",
        "security": [
          {
            "merchantJWT": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled — returns the updated buy_order row.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "403": {
            "description": "Buy-order belongs to another merchant."
          },
          "404": {
            "description": "Buy-order does not exist."
          },
          "409": {
            "description": "Buy-order is not in a cancellable status."
          }
        }
      }
    },
    "/v1/settlements/pending": {
      "get": {
        "tags": [
          "Settlements"
        ],
        "operationId": "getPendingSettlements",
        "summary": "Summary of unsettled accruals grouped by currency.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Pending summaries (one per currency).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PendingSummaryEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/settlements/report": {
      "get": {
        "tags": [
          "Settlements"
        ],
        "operationId": "getSettlementReport",
        "summary": "Aggregate settlement report for a date range.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "from",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Start of range (YYYY-MM-DD, inclusive)."
          },
          {
            "in": "query",
            "name": "to",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "End of range (YYYY-MM-DD, inclusive — extended server-side to end-of-day)."
          },
          {
            "in": "query",
            "name": "currency",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementReportEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Missing or malformed `from`/`to`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/settlements/export": {
      "get": {
        "tags": [
          "Settlements"
        ],
        "operationId": "exportSettlements",
        "summary": "Export settlements as JSON or CSV.",
        "description": "`format=pdf` is accepted but currently renders CSV under a different\nfilename (full PDF export is deferred to a future session).\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "from",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "in": "query",
            "name": "to",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "in": "query",
            "name": "currency",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "format",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv",
                "pdf"
              ],
              "default": "json"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Export payload in the requested format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementExportEnvelope"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "RFC 4180 CSV with formula-injection guarded cells."
                }
              }
            }
          },
          "400": {
            "description": "Missing or malformed `from`/`to`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/settlements/adjustments": {
      "get": {
        "tags": [
          "Settlements"
        ],
        "operationId": "listAdjustments",
        "summary": "List settlement adjustments for the authenticated merchant.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "status",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "approved",
                "rejected",
                "applied",
                "reversed"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Adjustments (possibly empty array).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementAdjustmentsEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/settlements/{id}": {
      "get": {
        "tags": [
          "Settlements"
        ],
        "operationId": "getSettlement",
        "summary": "Get a settlement with its line items.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SettlementID"
          }
        ],
        "responses": {
          "200": {
            "description": "Settlement with line items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementWithLineItemsEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Invalid ID format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Settlement not found, or not owned by this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/settlements/{id}/audit-log": {
      "get": {
        "tags": [
          "Settlements"
        ],
        "operationId": "getSettlementAuditLog",
        "summary": "Merchant-visible audit trail for a settlement.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SettlementID"
          }
        ],
        "responses": {
          "200": {
            "description": "Audit entries (possibly empty).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuditLogEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Invalid ID format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Settlement not found, or not owned by this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/settlements/{id}/retry": {
      "post": {
        "tags": [
          "Settlements"
        ],
        "operationId": "retrySettlement",
        "summary": "Retry a failed settlement by resetting it to `pending`.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SettlementID"
          }
        ],
        "responses": {
          "200": {
            "description": "Settlement reset to `pending` for re-payout.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Invalid ID format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Settlement not found, or not owned by this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Settlement is not in `failed` status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/settlements/{id}/dispute": {
      "post": {
        "tags": [
          "Settlements"
        ],
        "operationId": "disputeSettlement",
        "summary": "Open a dispute against a completed settlement.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SettlementID"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DisputeRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Dispute created as a new adjustment in `pending` status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementAdjustmentEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Invalid ID format or malformed body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Settlement not found, or not owned by this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Only `completed` settlements can be disputed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/deliveries": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "listWebhookDeliveries",
        "summary": "Recent webhook delivery attempts for this merchant.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Delivery attempts (possibly empty).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveriesEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "payment.matched": {
      "post": {
        "summary": "Fired when a payment has been matched to one or more sellers.",
        "description": "See `components.schemas.WebhookPayload` for the full envelope. Every\noutbound webhook is signed with HMAC-SHA256 over the canonical string\n`METHOD \\n PATH \\n TIMESTAMP \\n BODY`, hex-encoded. Verify using the\n`X-Webhook-Signature` and `X-Webhook-Timestamp` headers against the\nmerchant's webhook secret.\n",
        "operationId": "webhookPaymentMatched",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivery acknowledged. Any 2xx status ends the retry chain.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Arbitrary merchant-defined body; contents are ignored by TokenPay."
                }
              }
            }
          }
        }
      }
    },
    "payment.completed": {
      "post": {
        "summary": "Fired when a matched payment has been auto-released or seller-confirmed.",
        "operationId": "webhookPaymentCompleted",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivery acknowledged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "payment.failed": {
      "post": {
        "summary": "Fired when a payment fails (e.g. max rematch exceeded, seller failure).",
        "operationId": "webhookPaymentFailed",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivery acknowledged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "payment.expired": {
      "post": {
        "summary": "Fired when a lazy-expire transition flips a `matched`/`payment_pending` payment to `expired`.",
        "description": "Fires exactly once, on the edge of the state transition — not on\nsubsequent reads of an already-expired payment. See Session 082 for\nthe emit-site fix.\n",
        "operationId": "webhookPaymentExpired",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivery acknowledged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "settlement.completed": {
      "post": {
        "summary": "Fired when a settlement has been paid out successfully.",
        "operationId": "webhookSettlementCompleted",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivery acknowledged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "settlement.failed": {
      "post": {
        "summary": "Fired when a settlement payout attempt has failed.",
        "operationId": "webhookSettlementFailed",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivery acknowledged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "settlement.voided": {
      "post": {
        "summary": "Fired when a settlement has been voided (e.g. cancelled before payout).",
        "operationId": "webhookSettlementVoided",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivery acknowledged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "settlement.adjustment_applied": {
      "post": {
        "summary": "Fired when an approved adjustment has been applied to a settlement.",
        "operationId": "webhookSettlementAdjustmentApplied",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivery acknowledged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Merchant API key in the `X-API-Key` header. The first 10 characters\nare used as a DB lookup prefix; the full key is then bcrypt-verified.\n"
      }
    },
    "parameters": {
      "PaymentID": {
        "in": "path",
        "name": "id",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "Payment ID (UUID v4)."
      },
      "SettlementID": {
        "in": "path",
        "name": "id",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "Settlement ID (UUID v4)."
      },
      "PaymentStatusFilter": {
        "in": "query",
        "name": "status",
        "required": false,
        "schema": {
          "$ref": "#/components/schemas/PaymentStatus"
        }
      },
      "SettlementStatusFilter": {
        "in": "query",
        "name": "status",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "pending",
            "processing",
            "completed",
            "failed",
            "voided"
          ]
        }
      },
      "FromDate": {
        "in": "query",
        "name": "from",
        "required": false,
        "schema": {
          "type": "string",
          "format": "date-time"
        },
        "description": "Lower bound on `created_at` (RFC 3339)."
      },
      "ToDate": {
        "in": "query",
        "name": "to",
        "required": false,
        "schema": {
          "type": "string",
          "format": "date-time"
        },
        "description": "Upper bound on `created_at` (RFC 3339)."
      },
      "Limit": {
        "in": "query",
        "name": "limit",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 20
        }
      },
      "Offset": {
        "in": "query",
        "name": "offset",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        }
      },
      "Cursor": {
        "in": "query",
        "name": "cursor",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "Opaque cursor from `pagination.next_cursor` of a prior response."
      }
    },
    "schemas": {
      "Amount": {
        "type": "string",
        "pattern": "^-?\\d+$",
        "description": "Monetary amount in minor units (e.g. satang for THB, cents for AUD).\nSerialised as a JSON string to avoid JavaScript-number precision loss\n(see `StringAmount` in the Go model).\n"
      },
      "PaymentStatus": {
        "type": "string",
        "enum": [
          "created",
          "matching",
          "matched",
          "payment_pending",
          "payment_sent",
          "confirming",
          "completed",
          "failed",
          "expired",
          "refunded"
        ]
      },
      "Currency": {
        "type": "string",
        "enum": [
          "AUD",
          "USD",
          "THB",
          "SGD"
        ]
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "NOT_FOUND",
                  "VALIDATION_ERROR",
                  "INTERNAL_ERROR",
                  "CONFLICT",
                  "FORBIDDEN",
                  "UNAUTHORIZED",
                  "INSUFFICIENT_LIQUIDITY",
                  "API_KEY_EXPIRED",
                  "IP_NOT_WHITELISTED",
                  "NOT_TH",
                  "INTERNAL"
                ]
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "PaymentEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/GatewayPayment"
          }
        }
      },
      "PaymentWithMatchesEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/GatewayPaymentWithMatches"
          }
        }
      },
      "PaymentListEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data",
          "meta"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GatewayPayment"
            }
          },
          "meta": {
            "type": "object",
            "required": [
              "total",
              "limit",
              "offset"
            ],
            "properties": {
              "total": {
                "type": "integer"
              },
              "limit": {
                "type": "integer"
              },
              "offset": {
                "type": "integer"
              }
            }
          }
        }
      },
      "CheckoutEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "payment",
              "merchant"
            ],
            "properties": {
              "payment": {
                "type": "object",
                "required": [
                  "id",
                  "order_id",
                  "amount",
                  "currency",
                  "status",
                  "expires_at"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "order_id": {
                    "type": "string"
                  },
                  "amount": {
                    "$ref": "#/components/schemas/Amount"
                  },
                  "currency": {
                    "$ref": "#/components/schemas/Currency"
                  },
                  "status": {
                    "$ref": "#/components/schemas/PaymentStatus"
                  },
                  "return_url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "expires_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              },
              "merchant": {
                "type": "object",
                "required": [
                  "id"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        }
      },
      "ProofUploadEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "payment_id",
              "status",
              "message"
            ],
            "properties": {
              "payment_id": {
                "type": "string",
                "format": "uuid"
              },
              "status": {
                "$ref": "#/components/schemas/PaymentStatus"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "RatesEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Rates"
          }
        }
      },
      "ThaiBanksEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "banks"
            ],
            "properties": {
              "banks": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ThaiBank"
                }
              }
            }
          }
        }
      },
      "PromptPayGenerateEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "payload",
              "promptpay_id",
              "currency",
              "dynamic"
            ],
            "properties": {
              "payload": {
                "type": "string",
                "description": "EMVCo-formatted QR payload string."
              },
              "promptpay_id": {
                "type": "string"
              },
              "currency": {
                "type": "string",
                "const": "THB"
              },
              "dynamic": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "SettlementEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Settlement"
          }
        }
      },
      "SettlementListEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data",
          "pagination"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Settlement"
            }
          },
          "pagination": {
            "type": "object",
            "required": [
              "has_more"
            ],
            "properties": {
              "next_cursor": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Opaque cursor for the next page; `null` when `has_more` is false."
              },
              "has_more": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "SettlementWithLineItemsEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/SettlementWithLineItems"
          }
        }
      },
      "PendingSummaryEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PendingSummary"
            }
          }
        }
      },
      "SettlementReportEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/SettlementReport"
          }
        }
      },
      "SettlementExportEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "required": [
              "settlements",
              "generated_at",
              "format"
            ],
            "properties": {
              "settlements": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SettlementExportItem"
                }
              },
              "generated_at": {
                "type": "string",
                "format": "date-time"
              },
              "format": {
                "type": "string",
                "const": "json"
              }
            }
          }
        }
      },
      "SettlementAdjustmentsEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SettlementAdjustment"
            }
          }
        }
      },
      "SettlementAdjustmentEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/SettlementAdjustment"
          }
        }
      },
      "AuditLogEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AuditLogEntry"
            }
          }
        }
      },
      "WebhookDeliveriesEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data",
          "meta"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookDelivery"
            }
          },
          "meta": {
            "type": "object",
            "required": [
              "total",
              "limit",
              "offset"
            ],
            "properties": {
              "total": {
                "type": "integer"
              },
              "limit": {
                "type": "integer"
              },
              "offset": {
                "type": "integer"
              }
            }
          }
        }
      },
      "CreatePaymentRequest": {
        "type": "object",
        "required": [
          "order_id",
          "amount",
          "currency",
          "payment_methods"
        ],
        "properties": {
          "order_id": {
            "type": "string",
            "minLength": 1,
            "description": "Merchant-unique order ID."
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "description": "Amount in minor units. Sent as a JSON number on the request (Go\n`int64`); note that the response echoes it back via the\n`Amount` string type to preserve precision.\n"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "customer_email": {
            "type": "string",
            "format": "email"
          },
          "payment_methods": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            }
          },
          "notify_url": {
            "type": "string",
            "format": "uri"
          },
          "return_url": {
            "type": "string",
            "format": "uri"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Free-form merchant metadata echoed back on responses."
          }
        }
      },
      "PromptPayGenerateRequest": {
        "type": "object",
        "required": [
          "promptpay_id"
        ],
        "properties": {
          "promptpay_id": {
            "type": "string",
            "description": "Phone number or Thai national ID."
          },
          "amount": {
            "type": "number",
            "minimum": 0,
            "description": "Amount in THB (major units). Omit for a static QR."
          }
        }
      },
      "DisputeRequest": {
        "type": "object",
        "required": [
          "reason"
        ],
        "properties": {
          "reason": {
            "type": "string",
            "minLength": 1
          },
          "expected_amount": {
            "type": "integer",
            "format": "int64",
            "description": "Minor units."
          },
          "evidence_urls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          }
        }
      },
      "GatewayPayment": {
        "type": "object",
        "required": [
          "id",
          "merchant_id",
          "order_id",
          "amount",
          "currency",
          "status",
          "expires_at",
          "version",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "merchant_id": {
            "type": "string",
            "format": "uuid"
          },
          "aggregator_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "order_id": {
            "type": "string"
          },
          "sub_merchant_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "sub_merchant_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "upstream_trade_no": {
            "type": [
              "string",
              "null"
            ]
          },
          "amount": {
            "$ref": "#/components/schemas/Amount"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "checkout_url": {
            "type": "string",
            "format": "uri"
          },
          "notify_url": {
            "type": "string",
            "format": "uri"
          },
          "return_url": {
            "type": "string",
            "format": "uri"
          },
          "customer_email": {
            "type": "string",
            "format": "email"
          },
          "customer_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "match_type": {
            "type": "string"
          },
          "matched_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "failed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "failure_reason": {
            "type": "string"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "version": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "GatewayPaymentWithMatches": {
        "allOf": [
          {
            "$ref": "#/components/schemas/GatewayPayment"
          },
          {
            "type": "object",
            "properties": {
              "matches": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MatchInfo"
                }
              }
            }
          }
        ]
      },
      "MatchInfo": {
        "type": "object",
        "required": [
          "id",
          "trade_id",
          "seller_id",
          "allocated_fiat_amount",
          "allocated_token_amount",
          "rate",
          "match_order",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "trade_id": {
            "type": "string",
            "format": "uuid"
          },
          "seller_id": {
            "type": "string",
            "format": "uuid"
          },
          "allocated_fiat_amount": {
            "type": "integer",
            "format": "int64",
            "description": "Minor units."
          },
          "allocated_token_amount": {
            "type": "integer",
            "format": "int64"
          },
          "rate": {
            "type": "integer",
            "description": "Rate in basis-point units."
          },
          "match_order": {
            "type": "integer"
          },
          "status": {
            "type": "string"
          }
        }
      },
      "Rates": {
        "type": "object",
        "required": [
          "currency",
          "payment_method",
          "total_available_fiat",
          "seller_count",
          "best_rate",
          "estimated_token_amount"
        ],
        "properties": {
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "payment_method": {
            "type": "string"
          },
          "total_available_fiat": {
            "type": "integer",
            "format": "int64"
          },
          "seller_count": {
            "type": "integer"
          },
          "best_rate": {
            "type": "integer"
          },
          "estimated_token_amount": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "ThaiBank": {
        "type": "object",
        "required": [
          "code",
          "short_name",
          "name_en",
          "name_th"
        ],
        "properties": {
          "code": {
            "type": "string"
          },
          "short_name": {
            "type": "string"
          },
          "name_en": {
            "type": "string"
          },
          "name_th": {
            "type": "string"
          }
        }
      },
      "Settlement": {
        "type": "object",
        "required": [
          "id",
          "merchant_id",
          "period_start",
          "period_end",
          "total_volume",
          "total_fees",
          "net_amount",
          "currency",
          "status",
          "includes_sub_merchants",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "merchant_id": {
            "type": "string",
            "format": "uuid"
          },
          "aggregator_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "period_start": {
            "type": "string",
            "format": "date-time"
          },
          "period_end": {
            "type": "string",
            "format": "date-time"
          },
          "total_volume": {
            "type": "integer",
            "format": "int64"
          },
          "total_fees": {
            "type": "integer",
            "format": "int64"
          },
          "net_amount": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed",
              "voided"
            ]
          },
          "payout_reference": {
            "type": [
              "string",
              "null"
            ]
          },
          "payout_tx_hash": {
            "type": [
              "string",
              "null"
            ]
          },
          "includes_sub_merchants": {
            "type": "boolean"
          },
          "settled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SettlementWithLineItems": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Settlement"
          },
          {
            "type": "object",
            "required": [
              "line_items"
            ],
            "properties": {
              "line_items": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SettlementLineItem"
                }
              }
            }
          }
        ]
      },
      "SettlementLineItem": {
        "type": "object",
        "required": [
          "id",
          "payment_id",
          "merchant_id",
          "gross_amount",
          "platform_fee",
          "net_amount",
          "currency",
          "status",
          "completed_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "payment_id": {
            "type": "string",
            "format": "uuid"
          },
          "merchant_id": {
            "type": "string",
            "format": "uuid"
          },
          "gross_amount": {
            "type": "integer",
            "format": "int64"
          },
          "platform_fee": {
            "type": "integer",
            "format": "int64"
          },
          "net_amount": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "settlement_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "batched",
              "settled",
              "disputed",
              "resolved"
            ]
          },
          "completed_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PendingSummary": {
        "type": "object",
        "required": [
          "merchant_id",
          "currency",
          "pending_volume",
          "pending_fees",
          "pending_net",
          "pending_count"
        ],
        "properties": {
          "merchant_id": {
            "type": "string",
            "format": "uuid"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "pending_volume": {
            "type": "integer",
            "format": "int64"
          },
          "pending_fees": {
            "type": "integer",
            "format": "int64"
          },
          "pending_net": {
            "type": "integer",
            "format": "int64"
          },
          "pending_count": {
            "type": "integer"
          }
        }
      },
      "SettlementReport": {
        "type": "object",
        "required": [
          "period",
          "total_settlements",
          "total_volume",
          "total_fees",
          "total_net_paid",
          "total_pending",
          "by_status",
          "payout_methods"
        ],
        "properties": {
          "period": {
            "type": "object",
            "required": [
              "from",
              "to"
            ],
            "properties": {
              "from": {
                "type": "string"
              },
              "to": {
                "type": "string"
              }
            }
          },
          "currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "total_settlements": {
            "type": "integer"
          },
          "total_volume": {
            "type": "integer",
            "format": "int64"
          },
          "total_fees": {
            "type": "integer",
            "format": "int64"
          },
          "total_net_paid": {
            "type": "integer",
            "format": "int64"
          },
          "total_pending": {
            "type": "integer",
            "format": "int64"
          },
          "by_status": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/StatusAggregate"
            }
          },
          "average_settlement_time_hours": {
            "type": [
              "number",
              "null"
            ]
          },
          "payout_methods": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/StatusAggregate"
            }
          }
        }
      },
      "StatusAggregate": {
        "type": "object",
        "required": [
          "count",
          "net_amount"
        ],
        "properties": {
          "count": {
            "type": "integer"
          },
          "net_amount": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "SettlementExportItem": {
        "type": "object",
        "required": [
          "id",
          "merchant_id",
          "period_start",
          "period_end",
          "total_volume",
          "total_fees",
          "net_amount",
          "currency",
          "status",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "merchant_id": {
            "type": "string",
            "format": "uuid"
          },
          "period_start": {
            "type": "string",
            "format": "date-time"
          },
          "period_end": {
            "type": "string",
            "format": "date-time"
          },
          "total_volume": {
            "type": "integer",
            "format": "int64"
          },
          "total_fees": {
            "type": "integer",
            "format": "int64"
          },
          "net_amount": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "status": {
            "type": "string"
          },
          "payout_reference": {
            "type": [
              "string",
              "null"
            ]
          },
          "payout_tx_hash": {
            "type": [
              "string",
              "null"
            ]
          },
          "settled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SettlementAdjustment": {
        "type": "object",
        "required": [
          "id",
          "settlement_id",
          "adjustment_type",
          "amount",
          "currency",
          "reason",
          "status",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "settlement_id": {
            "type": "string",
            "format": "uuid"
          },
          "adjustment_type": {
            "type": "string"
          },
          "amount": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "reason": {
            "type": "string"
          },
          "evidence_urls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "applied",
              "reversed"
            ]
          },
          "requested_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "approved_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "applied_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AuditLogEntry": {
        "type": "object",
        "required": [
          "id",
          "settlement_id",
          "action",
          "actor_type",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "settlement_id": {
            "type": "string",
            "format": "uuid"
          },
          "action": {
            "type": "string"
          },
          "previous_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "new_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "actor_type": {
            "type": "string"
          },
          "actor_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "ip_address": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "required": [
          "id",
          "merchant_id",
          "payment_id",
          "event",
          "url",
          "response_status",
          "attempt",
          "success",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "merchant_id": {
            "type": "string",
            "format": "uuid"
          },
          "payment_id": {
            "type": "string",
            "format": "uuid"
          },
          "event": {
            "$ref": "#/components/schemas/WebhookEventName"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "request_body": {
            "type": "string"
          },
          "response_status": {
            "type": "integer"
          },
          "response_body": {
            "type": "string"
          },
          "attempt": {
            "type": "integer",
            "minimum": 1
          },
          "success": {
            "type": "boolean"
          },
          "error_message": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookEventName": {
        "type": "string",
        "description": "Canonical set of outbound webhook events. S100 introduces the\n`trade.*` aliases alongside the `payment.*` variants; for the\n90-day sunset window both are fired per lifecycle transition.\nPayloads are byte-identical except for this discriminator.\nThe `trade.*` variants are the preferred stream going forward;\n`payment.*` is tracked via the gateway-service metric\n`payment_path_legacy_hits_total` for adoption monitoring and will\nbe removed after `WEBHOOK_EVENT_ALIAS_SUNSET` (operator-set).\n",
        "enum": [
          "payment.matched",
          "payment.completed",
          "payment.failed",
          "payment.expired",
          "trade.matched",
          "trade.completed",
          "trade.failed",
          "trade.expired",
          "settlement.completed",
          "settlement.failed",
          "settlement.voided",
          "settlement.adjustment_applied"
        ]
      },
      "WebhookPayload": {
        "type": "object",
        "required": [
          "event",
          "payment_id",
          "order_id",
          "amount",
          "currency",
          "status",
          "idempotency_key",
          "timestamp"
        ],
        "description": "The unified outbound webhook envelope. Every event — payment.* and\nsettlement.* — uses this shape; the `event` discriminator tells the\nmerchant which state transition occurred. Optional fields\n(`auto_released`, `failure_reason`, `match_details`) are populated\nonly for the events that carry them.\n",
        "properties": {
          "event": {
            "$ref": "#/components/schemas/WebhookEventName"
          },
          "payment_id": {
            "type": "string",
            "format": "uuid"
          },
          "order_id": {
            "type": "string"
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "description": "Minor units. Note: unlike the REST response body, the webhook envelope uses a JSON number here."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "idempotency_key": {
            "type": "string",
            "description": "`<payment_id>:<event>` — stable per transition, safe to de-dup on."
          },
          "auto_released": {
            "type": "boolean",
            "description": "Only set on `payment.completed`."
          },
          "failure_reason": {
            "type": "string",
            "description": "Only set on `payment.failed` / `settlement.failed`."
          },
          "match_details": {
            "type": "array",
            "description": "Only set on `payment.matched`.",
            "items": {
              "$ref": "#/components/schemas/MatchInfo"
            }
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ReceiverConfirmRequest": {
        "type": "object",
        "description": "S102 (PR #125) request body for POST /v1/pending-funds/{id}/receiver-confirm.\nThe receiver attaches their own receipt evidence; the gateway runs it\nthrough the configured ReceiptValidator (EasySlip in TH; Null in AU).\n",
        "required": [
          "content_type",
          "receipt_base64"
        ],
        "properties": {
          "content_type": {
            "type": "string",
            "enum": [
              "image/png",
              "image/jpeg",
              "application/pdf",
              "application/json"
            ],
            "description": "Wire format of the receipt bytes. Mirrors receipt-validator.ContentType."
          },
          "receipt_base64": {
            "type": "string",
            "description": "Base64-encoded receipt bytes. Must decode to a non-empty byte\nstring; otherwise the request is rejected with\n`RECEIVER_CONFIRM_MALFORMED_RECEIPT` (400) before any validator\ncall is made.\n",
            "example": "iVBORw0KGgoAAAANSUhEUgAA..."
          }
        }
      },
      "ReceiverConfirmResult": {
        "type": "object",
        "description": "S102 (PR #125) response body. Wraps the freshly inserted\n`receiver_confirm_audit` row's id + the resolved outcome alongside the\nupdated `pending_funds` row so the client can render the new state\nwithout a follow-up GET.\n\nNote: the HTTP status code is 201 even when `outcome` is `failed` or\n`unverified`. The 201 signals that an audit row was recorded; clients\nMUST inspect `outcome` for the substantive result. (`malformed` is the\nsole outcome that does NOT produce an audit row — it is rejected with\n400 before reaching the validator.)\n",
        "required": [
          "pending_funds",
          "audit_id",
          "outcome",
          "attempt_index",
          "attempts_remaining"
        ],
        "properties": {
          "pending_funds": {
            "description": "The updated pending_funds row, post-decision. Status is `confirmed`\non the verified path, `failed` on validator-fail or auto-fail at\nretry cap, or `validated_pending_receiver_confirm` (unchanged) on\nunverified / timeout under the cap.\n",
            "type": "object",
            "additionalProperties": true
          },
          "audit_id": {
            "type": "string",
            "format": "uuid",
            "description": "Newly inserted `receiver_confirm_audit` row ID. Stable; clients can deep-link the admin audit detail page (S103 WS5)."
          },
          "outcome": {
            "type": "string",
            "enum": [
              "verified",
              "failed",
              "unverified",
              "timeout",
              "malformed"
            ],
            "description": "Audit-row outcome (see `receiver_confirm_outcome` enum in mig 059).\n`malformed` will never appear in a 201 response — it is surfaced\nonly as a 400 error code.\n"
          },
          "attempt_index": {
            "type": "integer",
            "minimum": 1,
            "description": "This call's position in the retry sequence (1-based). Equals `validator_attempt_count` after the bump."
          },
          "attempts_remaining": {
            "type": "integer",
            "minimum": 0,
            "description": "Floor-zero remaining retries before the row will be auto-failed\non a non-verified, non-timeout outcome. Computed as\n`max(0, RECEIVER_CONFIRM_MAX_ATTEMPTS - attempt_index)`.\n"
          },
          "failure_code": {
            "type": "string",
            "nullable": true,
            "description": "Populated when `outcome != verified`. Maps validator FailureCode\nto a stable application code (e.g. `qr_mismatch`, `provider_timeout`,\n`provider_unavailable`, `unsupported_content_type`, `validator_fail`,\n`unverifiable`).\n"
          },
          "failure_message": {
            "type": "string",
            "nullable": true,
            "description": "Optional human-readable failure context. Capped at 256 chars so\nunbounded provider error strings cannot bloat the audit row.\n"
          }
        }
      },
      "SellOrderStatus": {
        "type": "string",
        "enum": [
          "pending",
          "matched",
          "cash_leg_in_flight",
          "completed",
          "failed"
        ],
        "description": "Sell-order lifecycle (mig 060 enum `sell_order_status`). 'disputed'\nis intentionally absent at S103(a); added in S103(b) mig 061\n(see ADR-103a-2).\n"
      },
      "SellOrder": {
        "type": "object",
        "description": "Canonical row representation of `sell_orders` (mig 060). Mirrors\n`model.SellOrder` in `platform-api/services/gateway-service/internal/model/sell_orders.go`.\n`amount_pay` is shipped as a decimal string to preserve NUMERIC(20,6)\nprecision. `target_fiat_amount` + `target_currency` are AUDIT-ONLY\n(POSTURE LAW: TokenPay never touches fiat).\n",
        "required": [
          "id",
          "merchant_id",
          "amount_pay",
          "cash_leg_destination",
          "status",
          "created_at",
          "expires_at",
          "cash_leg_attempts",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "merchant_id": {
            "type": "string",
            "format": "uuid"
          },
          "amount_pay": {
            "type": "string",
            "description": "NUMERIC(20,6) micro-PAY shipped as decimal string."
          },
          "target_fiat_amount": {
            "type": "string",
            "nullable": true,
            "description": "Audit-only; TokenPay does not initiate the fiat transfer."
          },
          "target_currency": {
            "type": "string",
            "nullable": true,
            "description": "ISO-4217; co-required with target_fiat_amount."
          },
          "cash_leg_destination": {
            "type": "string",
            "description": "Merchant-owned free-text. NEVER read, parsed, or transmitted by TokenPay."
          },
          "status": {
            "$ref": "#/components/schemas/SellOrderStatus"
          },
          "matched_buy_order_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Plain UUID NULL; FK deferred (ADR-103a-3)."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Handler-set on create from SELL_ORDER_PENDING_TTL_HOURS (Q2 ADR-103a-1; no SQL DEFAULT)."
          },
          "cash_leg_attempts": {
            "type": "integer",
            "minimum": 0
          },
          "last_cash_leg_attempt_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "failure_reason": {
            "type": "string",
            "nullable": true,
            "description": "Free-text. S103(a) emits: expired_unmatched, cash_leg_retry_exhausted, cancelled_by_merchant."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SellOrderListResponse": {
        "type": "object",
        "description": "Cursor-paginated list shape returned by `GET /v1/merchant-sell-orders`.",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SellOrder"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Opaque cursor for the next page; absent/null when no more rows."
          }
        }
      },
      "SellOrderCreateRequest": {
        "type": "object",
        "description": "Body for `POST /v1/merchant-sell-orders`. Mirrors\n`model.SellOrderCreateRequest`. `amount_pay` is a decimal string\n(NUMERIC(20,6) precision). `Idempotency-Key` is a required HEADER —\nnot part of this body.\n",
        "required": [
          "amount_pay",
          "cash_leg_destination"
        ],
        "properties": {
          "amount_pay": {
            "type": "string",
            "description": "NUMERIC(20,6) micro-PAY as decimal string. Must be > 0."
          },
          "target_fiat_amount": {
            "type": "string",
            "nullable": true,
            "description": "Audit-only informational hint. TokenPay never initiates fiat transfer."
          },
          "target_currency": {
            "type": "string",
            "nullable": true,
            "description": "ISO-4217. Co-required with target_fiat_amount."
          },
          "cash_leg_destination": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048,
            "description": "Merchant-owned free-text. Never parsed by TokenPay."
          }
        }
      },
      "SellOrderCashLegConfirmRequest": {
        "type": "object",
        "description": "Body for `POST /v1/merchant-sell-orders/{id}/confirm-cash-leg`.\nMirrors `model.SellOrderConfirmCashLegRequest`. `Idempotency-Key`\nis a required HEADER, not part of this body.\n",
        "required": [
          "external_reference"
        ],
        "properties": {
          "external_reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256,
            "description": "Free-text merchant-supplied reference (wire confirmation ID,\nPSP transaction id, in-house cash-disbursement id). Stored\nverbatim on the row's audit (webhook payload + admin diff).\n"
          }
        }
      },
      "ReceiverConfirmAuditRow": {
        "type": "object",
        "description": "Canonical row representation of `receiver_confirm_audit` (mig 059,\nS102 PR #125). Mirrors `model.ReceiverConfirmAuditRow`. Append-only\nper ADR-102-2; this row is never UPDATEd or DELETEd by the gateway.\n",
        "required": [
          "id",
          "pending_funds_id",
          "validator_kind",
          "outcome",
          "validator_latency_ms",
          "decided_at",
          "attempt_index",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "pending_funds_id": {
            "type": "string",
            "format": "uuid"
          },
          "validator_kind": {
            "type": "string",
            "description": "e.g. 'null_validator', 'easyslip'."
          },
          "outcome": {
            "type": "string",
            "enum": [
              "verified",
              "failed",
              "unverified",
              "timeout",
              "malformed"
            ],
            "description": "Mirrors enum `receiver_confirm_outcome` (mig 059). 'malformed' never persisted (caller-side reject)."
          },
          "validator_response": {
            "description": "Raw JSONB validator response. Audit-only; fiat values therein never cross the ledger boundary.",
            "type": "object",
            "additionalProperties": true,
            "nullable": true
          },
          "validator_latency_ms": {
            "type": "integer",
            "minimum": 0
          },
          "failure_code": {
            "type": "string",
            "nullable": true
          },
          "failure_message": {
            "type": "string",
            "nullable": true,
            "description": "Capped at 256 chars."
          },
          "decided_at": {
            "type": "string",
            "format": "date-time"
          },
          "decided_by_user_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "attempt_index": {
            "type": "integer",
            "minimum": 1
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ReceiverConfirmAuditListResponse": {
        "type": "object",
        "description": "List shape returned by `GET /admin/audit/receiver-confirm`.\nMirrors `handler.ReceiverConfirmAuditListResponse` in\n`platform-api/services/gateway-service/internal/handler/receiver_confirm_admin.go`.\n",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReceiverConfirmAuditRow"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Opaque cursor for the next page; absent/null when no more rows."
          }
        }
      },
      "GatewayDisputeKind": {
        "type": "string",
        "enum": [
          "cash_leg_failure",
          "merchant_dispute",
          "chargeback"
        ],
        "description": "Kind of dispute. `cash_leg_failure` is auto-opened by Phase 2A WS3 from\nsell_order_cash_leg.go; `merchant_dispute` is merchant-initiated via\nPOST /v1/merchant-disputes; `chargeback` is reserved for future bank-side\nreversals (not yet wired in Phase 1).\n"
      },
      "GatewayDisputeStatus": {
        "type": "string",
        "enum": [
          "opened",
          "investigating",
          "resolved",
          "escalated"
        ],
        "description": "Gateway dispute lifecycle (Q4 locked): opened → investigating → (resolved | escalated).\n`resolved` and `escalated` are terminal.\n"
      },
      "GatewayDisputeResolutionOutcome": {
        "type": "string",
        "enum": [
          "merchant_favored",
          "counterparty_favored",
          "no_action",
          "escalated"
        ],
        "description": "Super-admin verdict on ResolveDispute. Three of four outcomes resolve\nto status `resolved`; only `escalated` resolves to `escalated`.\n"
      },
      "GatewayDisputeEvent": {
        "type": "object",
        "description": "One row in the append-only gateway_dispute_events audit table. Never UPDATE\nor DELETE — the migration's trigger raises on attempted mutation.\n",
        "required": [
          "id",
          "gateway_dispute_id",
          "event_kind",
          "details",
          "recorded_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "gateway_dispute_id": {
            "type": "string",
            "format": "uuid"
          },
          "event_kind": {
            "type": "string",
            "description": "Free-text VARCHAR(64) at DB layer for additivity. Phase 1 labels:\nmerchant_opened, cash_leg_auto_opened, investigating_started,\nresolved, escalated, note_added.\n"
          },
          "actor_user_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "details": {
            "type": "object",
            "additionalProperties": true,
            "description": "JSONB blob whose schema varies by event_kind."
          },
          "recorded_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "GatewayDispute": {
        "type": "object",
        "description": "Canonical row representation of the gateway_disputes table from migration 061.\nField order matches repository gatewayDisputeColumns scan order.\n",
        "required": [
          "id",
          "sell_order_id",
          "kind",
          "status",
          "opened_at",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "sell_order_id": {
            "type": "string",
            "format": "uuid"
          },
          "pending_funds_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "kind": {
            "$ref": "#/components/schemas/GatewayDisputeKind"
          },
          "status": {
            "$ref": "#/components/schemas/GatewayDisputeStatus"
          },
          "opened_at": {
            "type": "string",
            "format": "date-time"
          },
          "resolved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "resolution_actor_user_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "resolution_notes": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "GatewayDisputeOpenRequest": {
        "type": "object",
        "description": "Merchant-side create payload for POST /v1/merchant-disputes.\nIdempotency-Key is read from the HTTP header (not body).\n",
        "required": [
          "sell_order_id",
          "notes"
        ],
        "properties": {
          "sell_order_id": {
            "type": "string",
            "format": "uuid"
          },
          "notes": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4000,
            "description": "Resolution rationale; bounded by dispute.max_resolution_notes_length seed."
          }
        }
      },
      "GatewayDisputeResolveRequest": {
        "type": "object",
        "description": "Super-admin payload for POST /admin/disputes/:id/resolve.\nOutcome drives terminal status; notes lands in resolution_notes verbatim.\n",
        "required": [
          "outcome",
          "notes"
        ],
        "properties": {
          "outcome": {
            "$ref": "#/components/schemas/GatewayDisputeResolutionOutcome"
          },
          "notes": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4000
          }
        }
      },
      "GatewayDisputeListResponse": {
        "type": "object",
        "description": "Merchant-list shape (GET /v1/merchant-disputes). Cursor pagination\nmatches SellOrderListResponse precedent.\n",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GatewayDispute"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "RFC3339Nano opened_at of the last item; pass to ?cursor for next page."
          }
        }
      },
      "GatewayDisputeAdminListResponse": {
        "type": "object",
        "description": "Admin-list shape (GET /admin/disputes). Same envelope as the merchant\nvariant; held distinct so future admin-only fields can be added without\nbreaking the merchant contract.\n",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GatewayDispute"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "GatewayDisputeDetailResponse": {
        "type": "object",
        "description": "Admin detail shape (GET /admin/disputes/:id), pairing the disputes\nrow with its full append-only event timeline.\n",
        "required": [
          "dispute",
          "events"
        ],
        "properties": {
          "dispute": {
            "$ref": "#/components/schemas/GatewayDispute"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GatewayDisputeEvent"
            }
          }
        }
      },
      "GatewayDisputeQueueResponse": {
        "type": "object",
        "description": "Manual-ops \"stuck PAY\" queue (GET /admin/disputes/queue).\nstatus IN (opened, investigating), age-sorted ascending.\n",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GatewayDispute"
            }
          }
        }
      }
    }
  },
  "x-fx-service-paths": {
    "/fx/quote": {
      "post": {
        "tags": [
          "FX"
        ],
        "summary": "Lock a rate for a merchant + currency pair",
        "description": "Issues a rate lock for `fx.quote_lock_seconds` (default 3600s). The\n`locked_rate` is honored regardless of subsequent market moves. Served\nby `fx-service:4003` (moved from `:3011` in S099 BUG-192).\n\nErrors:\n  * `400` missing / malformed body, invalid `merchant_id` UUID\n  * `404 no_rate_for_pair` when the pair is not in `FX_PAIRS`\n  * `503 fx_service_disabled` when `FX_SERVICE_ENABLED=false`\n  * `503 pair_quarantined` when the kill-switch has tripped for the pair\n  * `503 rate_stale` when the latest tick is older than the stale threshold\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "merchant_id",
                  "currency_pair"
                ],
                "properties": {
                  "merchant_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "currency_pair": {
                    "type": "string",
                    "example": "USD/THB"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Quote locked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/x-fx-service-schemas/FXQuote"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request."
          },
          "404": {
            "description": "No rate for pair."
          },
          "503": {
            "description": "Service disabled / pair quarantined / rate stale."
          }
        }
      }
    },
    "/fx/quote/{id}": {
      "get": {
        "tags": [
          "FX"
        ],
        "summary": "Retrieve a previously-locked quote",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Quote found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/x-fx-service-schemas/FXQuote"
                }
              }
            }
          },
          "400": {
            "description": "Invalid UUID."
          },
          "404": {
            "description": "quote_not_found."
          },
          "503": {
            "description": "fx_service_disabled."
          }
        }
      }
    },
    "/fx/rates": {
      "get": {
        "tags": [
          "FX"
        ],
        "summary": "Latest tick per configured pair + current peg metadata (admin observability)",
        "responses": {
          "200": {
            "description": "Rates snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/x-fx-service-schemas/FXRate"
                      }
                    },
                    "peg_currency": {
                      "type": "string",
                      "example": "USD"
                    },
                    "peg_rate": {
                      "type": "number",
                      "example": 1
                    },
                    "peg_source": {
                      "type": "string",
                      "enum": [
                        "fixed",
                        "oracle",
                        "market"
                      ]
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "fx_service_disabled."
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "tags": [
          "FX"
        ],
        "summary": "fx-service liveness",
        "description": "Reports `\"ok\"` when `FX_SERVICE_ENABLED=true`, `\"disabled\"` otherwise.\nReturns 200 in both cases so k8s/ALB liveness probes pass while\nallowing operators to see the flag state from the response body.\n",
        "responses": {
          "200": {
            "description": "Health status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "disabled"
                      ]
                    },
                    "service": {
                      "type": "string",
                      "example": "fx-service"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "x-fx-service-schemas": {
    "FXQuote": {
      "type": "object",
      "description": "Locked FX quote row. PK is `quote_id`.",
      "required": [
        "quote_id",
        "merchant_id",
        "currency_pair",
        "locked_rate",
        "rate_source",
        "expires_at",
        "created_at"
      ],
      "properties": {
        "quote_id": {
          "type": "string",
          "format": "uuid"
        },
        "merchant_id": {
          "type": "string",
          "format": "uuid"
        },
        "currency_pair": {
          "type": "string",
          "example": "USD/THB"
        },
        "locked_rate": {
          "type": "string",
          "description": "NUMERIC(20,10) serialized as string to preserve precision. Right-padded to 10 decimal places.",
          "example": "32.4000820000"
        },
        "rate_source": {
          "type": "string",
          "description": "Upstream provider that produced this rate.",
          "example": "open.er-api.com"
        },
        "expires_at": {
          "type": "string",
          "format": "date-time"
        },
        "created_at": {
          "type": "string",
          "format": "date-time"
        }
      }
    },
    "FXRate": {
      "type": "object",
      "required": [
        "currency_pair",
        "rate",
        "source",
        "fetched_at"
      ],
      "properties": {
        "currency_pair": {
          "type": "string",
          "example": "USD/THB"
        },
        "rate": {
          "type": "string",
          "example": "32.4000820000"
        },
        "source": {
          "type": "string",
          "example": "open.er-api.com"
        },
        "fetched_at": {
          "type": "string",
          "format": "date-time"
        },
        "edge_triggered:{ type": "boolean }"
      }
    },
    "MerchantBalanceEnvelope": {
      "type": "object",
      "required": [
        "success",
        "data"
      ],
      "properties": {
        "success": {
          "type": "boolean",
          "example": true
        },
        "data": {
          "$ref": "#/x-fx-service-schemas/MerchantBalance"
        }
      }
    },
    "MerchantBalance": {
      "type": "object",
      "description": "S101 merchant balance + deficit projection. All `*_micro_pay` fields are\nmicro-PAY (BIGINT; 1 PAY = 1,000,000 micro-PAY). `currency` is fixed to\n\"PAY\" today; multi-currency wallets are an S102+ concern.\n",
      "required": [
        "merchant_id",
        "user_id",
        "currency",
        "available_micro_pay",
        "buffer_micro_pay",
        "net_obligation_micro_pay",
        "deficit_micro_pay",
        "apr_bps",
        "entry_fee_usd",
        "countdown_days"
      ],
      "properties": {
        "merchant_id": {
          "type": "string",
          "format": "uuid"
        },
        "user_id": {
          "type": "string",
          "format": "uuid"
        },
        "currency": {
          "type": "string",
          "enum": [
            "PAY"
          ],
          "example": "PAY"
        },
        "available_micro_pay": {
          "type": "integer",
          "format": "int64",
          "example": 50000000000
        },
        "buffer_micro_pay": {
          "type": "integer",
          "format": "int64",
          "example": 100000000
        },
        "net_obligation_micro_pay": {
          "type": "integer",
          "format": "int64",
          "example": 60000000000
        },
        "deficit_micro_pay": {
          "type": "integer",
          "format": "int64",
          "description": "GREATEST(0, net_obligation − available − buffer).",
          "example": 9900000000
        },
        "apr_bps": {
          "type": "integer",
          "description": "Penalty APR in bps. Default 2000 (20% APR).",
          "example": 2000
        },
        "entry_fee_usd": {
          "type": "integer",
          "description": "Flat entry fee in USD; settled in PAY at prevailing rate. Default $50.",
          "example": 50
        },
        "countdown_days": {
          "type": "integer",
          "description": "Operator-set grace period days before further penalty escalation. Default 3.",
          "example": 3
        }
      }
    }
  },
  "x-admin-service-paths": {
    "/admin/deficit": {
      "get": {
        "tags": [
          "Admin",
          "Deficit"
        ],
        "summary": "List merchant deficit rows across all merchants.",
        "description": "Paginated list of `merchant_deficit_v` rows (LEFT JOINed with\n`merchant_buffer_state` for `trailing_30d_volume_micro_pay`).\nSorted by `deficit_micro_pay DESC` so the worst offenders surface\nfirst. Read-only. Super-admin only.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "deficits_only",
            "description": "When true, only rows where deficit_micro_pay > 0 are returned.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rows",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/x-admin-service-schemas/MerchantDeficitRow"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "example": 7
                    },
                    "limit": {
                      "type": "integer",
                      "example": 50
                    },
                    "offset": {
                      "type": "integer",
                      "example": 0
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Zod validation failure (limit > 200, offset < 0, etc)."
          },
          "401": {
            "description": "Missing / malformed / expired JWT."
          },
          "403": {
            "description": "Caller is not super_admin."
          }
        }
      }
    },
    "/admin/deficit/{merchantId}": {
      "get": {
        "tags": [
          "Admin",
          "Deficit"
        ],
        "summary": "Single-merchant deficit detail row.",
        "description": "Detail row for one merchant. Returns 404 when the merchant id does\nnot exist. Returns `{row: null, merchant_id}` when the merchant\nexists but has no PAY row in `merchant_deficit_v` yet (new merchant,\nno settlement activity).\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "merchantId",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Detail row OR empty-state object.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": [
                        "row"
                      ],
                      "properties": {
                        "row": {
                          "$ref": "#/x-admin-service-schemas/MerchantDeficitRow"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "row",
                        "merchant_id"
                      ],
                      "properties": {
                        "row": {
                          "type": "null"
                        },
                        "merchant_id": {
                          "type": "string",
                          "format": "uuid"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "merchantId is not a valid UUID."
          },
          "401": {
            "description": "Missing / malformed / expired JWT."
          },
          "403": {
            "description": "Caller is not super_admin."
          },
          "404": {
            "description": "Merchant not found.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "MERCHANT_NOT_FOUND"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/admin/fees/override": {
      "post": {
        "tags": [
          "Admin",
          "Fees"
        ],
        "summary": "Create or replace a fee_schedules row (any of the 3 layers)",
        "description": "Atomically expire any currently-live row for the same `(tier,\ncurrency_pair, volume_tier, override_merchant_id)` tuple and insert\nthe replacement. Writes `admin_audit_log` best-effort with\n`action='fee_override'`. Super-admin only.\n\nLayer mapping (mirrors fee-engine resolver):\n  * `volume_tier` null + `override_merchant_id` null → Layer 1 tier default\n  * `volume_tier` set + `override_merchant_id` null → Layer 2 volume bucket\n  * `override_merchant_id` set → Layer 3 merchant override\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/x-admin-service-schemas/FeeOverrideRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Inserted row.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "$ref": "#/x-admin-service-schemas/FeeSchedule"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Zod validation failure (missing field, max<min, volume_multiplier_bps without volume_tier, etc) OR malformed JSON body → { code: MALFORMED_JSON } via shared middleware (BUG-190)."
          },
          "401": {
            "description": "Missing / malformed / expired JWT."
          },
          "403": {
            "description": "Caller is not super_admin."
          }
        }
      }
    },
    "/admin/fx/killswitch-config": {
      "post": {
        "tags": [
          "Admin",
          "FX"
        ],
        "summary": "Update fx-service kill-switch threshold and/or enabled flag",
        "description": "Transactional UPSERT of `fx.kill_switch_threshold_bps` (required,\n0..10000) and optionally `fx.kill_switch_enabled` (boolean) in\n`system_config`. Writes `admin_audit_log` with\n`action='fx_killswitch_config'`. fx-service's 30s sysconfig poller\npicks up the new values with no restart. Super-admin only.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "threshold_bps"
                ],
                "properties": {
                  "threshold_bps": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 10000,
                    "example": 300
                  },
                  "enabled": {
                    "type": "boolean",
                    "example": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Config updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "threshold_bps": {
                      "type": "integer",
                      "example": 300
                    },
                    "enabled": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Zod validation failure OR malformed JSON body → { code: MALFORMED_JSON }."
          },
          "401": {
            "description": "Missing / malformed / expired JWT."
          },
          "403": {
            "description": "Caller is not super_admin."
          }
        }
      }
    }
  },
  "x-admin-service-schemas": {
    "MerchantDeficitRow": {
      "type": "object",
      "description": "Row from `merchant_deficit_v` LEFT JOINed with `merchant_buffer_state`.\nIncludes the admin-only fields the merchant-side\n`/v1/merchants/{id}/balance` does NOT expose.\n",
      "required": [
        "merchant_id",
        "merchant_user_id",
        "business_name",
        "currency",
        "available_micro_pay",
        "buffer_micro_pay",
        "net_obligation_micro_pay",
        "deficit_micro_pay",
        "apr_bps",
        "entry_fee_usd",
        "countdown_days",
        "trailing_30d_volume_micro_pay"
      ],
      "properties": {
        "merchant_id": {
          "type": "string",
          "format": "uuid"
        },
        "merchant_user_id": {
          "type": "string",
          "format": "uuid"
        },
        "business_name": {
          "type": "string"
        },
        "currency": {
          "type": "string",
          "enum": [
            "PAY"
          ]
        },
        "available_micro_pay": {
          "type": "integer",
          "format": "int64"
        },
        "buffer_micro_pay": {
          "type": "integer",
          "format": "int64"
        },
        "net_obligation_micro_pay": {
          "type": "integer",
          "format": "int64"
        },
        "deficit_micro_pay": {
          "type": "integer",
          "format": "int64"
        },
        "apr_bps": {
          "type": "integer"
        },
        "entry_fee_usd": {
          "type": "integer"
        },
        "countdown_days": {
          "type": "integer"
        },
        "buffer_threshold_micro_pay": {
          "type": "integer",
          "format": "int64",
          "nullable": true,
          "description": "Admin-only. From merchant_buffer_state."
        },
        "buffer_last_recalc_at": {
          "type": "string",
          "format": "date-time",
          "nullable": true,
          "description": "Admin-only. Last reconciliation worker recompute."
        },
        "trailing_30d_volume_micro_pay": {
          "type": "integer",
          "format": "int64",
          "description": "Admin-only. Trailing 30 UTC-day settled volume."
        }
      }
    },
    "FeeOverrideRequest": {
      "type": "object",
      "required": [
        "tier",
        "currency_pair",
        "base_bps",
        "min_fee_pay",
        "max_fee_pay",
        "formula_version"
      ],
      "properties": {
        "tier": {
          "type": "string",
          "example": "tier_1"
        },
        "currency_pair": {
          "type": "string",
          "pattern": "^PAY/[A-Z0-9-]+$",
          "example": "PAY/THB"
        },
        "base_bps": {
          "type": "integer",
          "minimum": 0,
          "maximum": 10000,
          "example": 100
        },
        "min_fee_pay": {
          "type": "integer",
          "description": "Micro-PAY (BIGINT).",
          "example": 100000
        },
        "max_fee_pay": {
          "type": "integer",
          "description": "Micro-PAY (BIGINT). Must be >= min_fee_pay.",
          "example": 50000000
        },
        "volume_tier": {
          "type": "string",
          "nullable": true,
          "description": "Layer 2 selector. Both-or-neither with volume_multiplier_bps.",
          "example": "bucket_10k"
        },
        "volume_multiplier_bps": {
          "type": "integer",
          "nullable": true,
          "description": "Layer 2. effective_bps = base_bps * volume_multiplier_bps / 10000.",
          "example": 8500
        },
        "merchant_class": {
          "type": "string",
          "nullable": true
        },
        "effective_from": {
          "type": "string",
          "format": "date-time",
          "nullable": true,
          "description": "Defaults to NOW() on insert."
        },
        "effective_until": {
          "type": "string",
          "format": "date-time",
          "nullable": true,
          "description": "Must be > effective_from when both present."
        },
        "override_merchant_id": {
          "type": "string",
          "format": "uuid",
          "nullable": true,
          "description": "Layer 3 selector. FK to merchants(id)."
        },
        "formula_version": {
          "type": "string",
          "example": "v1"
        },
        "notes": {
          "type": "string",
          "nullable": true
        }
      }
    },
    "FeeSchedule": {
      "type": "object",
      "description": "Row in fee_schedules table. All 12 PO-locked columns + id / created_at / updated_at.",
      "required": [
        "id",
        "tier",
        "currency_pair",
        "base_bps",
        "min_fee_pay",
        "max_fee_pay",
        "formula_version",
        "created_at",
        "updated_at"
      ],
      "properties": {
        "id": {
          "type": "string",
          "format": "uuid"
        },
        "tier": {
          "type": "string"
        },
        "currency_pair": {
          "type": "string"
        },
        "base_bps": {
          "type": "integer"
        },
        "min_fee_pay": {
          "type": "integer"
        },
        "max_fee_pay": {
          "type": "integer"
        },
        "volume_tier": {
          "type": "string",
          "nullable": true
        },
        "volume_multiplier_bps": {
          "type": "integer",
          "nullable": true
        },
        "merchant_class": {
          "type": "string",
          "nullable": true
        },
        "effective_from": {
          "type": "string",
          "format": "date-time"
        },
        "effective_until": {
          "type": "string",
          "format": "date-time",
          "nullable": true
        },
        "override_merchant_id": {
          "type": "string",
          "format": "uuid",
          "nullable": true
        },
        "formula_version": {
          "type": "string",
          "example": "v1"
        },
        "notes": {
          "type": "string",
          "nullable": true
        },
        "created_at": {
          "type": "string",
          "format": "date-time"
        },
        "updated_at": {
          "type": "string",
          "format": "date-time"
        }
      }
    }
  }
}
