{
  "openapi": "3.1.0",
  "info": {
    "title": "p2p Payment Gateway — Public API",
    "version": "1.0.0",
    "summary": "Merchant server-to-server payments API (payins, payouts, settlements, balance).",
    "description": "The public `/v1` API is for merchant server-to-server integration. Every request is authenticated with a secret **API key** (`Authorization: Bearer sk_test_...` or `sk_live_...`), gated by a per-merchant **IP allowlist**, and **rate limited**. HMAC request signing is an optional, additive gate for API keys in either environment. When signing is enabled for a key, every request made with that key must also send `X-Signature-Timestamp` and `X-Signature`; signing never replaces Bearer authentication, the IP allowlist, rate limiting, or mandatory idempotency. The key prefix selects the server-owned financial environment. Financial resources and idempotency are isolated by environment; shared merchant resources are called out on their operations. All money is an **integer number of the named currency's minor units**, serialized as JSON numbers, never floats. Currency metadata and operation readiness are server-owned; balances and every aggregate stay isolated by currency and the gateway performs no implicit FX. Every `POST` request requires an `Idempotency-Key` header. This is a separate surface from the dashboard `/api` tree, which uses cookie sessions; the two never share credentials.",
    "contact": {
      "name": "p2p developer support"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.example.com",
      "description": "Placeholder base URL — use the host issued to your account. The key prefix (sk_test_ / sk_live_) selects the environment; both share one base URL."
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Payins",
      "description": "Inbound payments — money a customer sends to you."
    },
    {
      "name": "Payouts",
      "description": "Outbound payments — money you send from your balance."
    },
    {
      "name": "Settlements",
      "description": "Sweeps from your balance to a verified destination."
    },
    {
      "name": "Balance",
      "description": "Spendable balance, per currency."
    },
    {
      "name": "Sandbox",
      "description": "Test-only balance funding and integration tools."
    },
    {
      "name": "Customers",
      "description": "Counterparty bank accounts — payout and settlement destinations."
    }
  ],
  "paths": {
    "/v1/payins": {
      "parameters": [
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "post": {
        "tags": [
          "Payins"
        ],
        "summary": "Create a payin",
        "operationId": "createPayin",
        "description": "Create an inbound payment request in the API key's environment. Scope `payins:write`. **Idempotent**: an `Idempotency-Key` header is required. `order_ref` must be unique per merchant and environment; the same value may be used independently in test and live. When `receiving_type` is `p2p` and direct matching dispatch is enabled, admission claims a pending P2P payout with the same currency and gross amount; if none is available, or matching is off, the payin is admitted on a fallback collection rail in the operator-configured order (System settings, P2P tab: bank transfer then QR by default, QR then bank, or off). The request fails with `422` (`P2P_LIQUIDITY_UNAVAILABLE`) only when fallback is off or no collection rail can accept it. **Channel**: `channel` selects the rail — `bank` (the default when omitted), `qrcode`, or `crypto` — as a shortcut for `provider`/`receiving_type`; when set it overrides them. `bank` and `qrcode` return a `PayinView` with a hosted-pay `pay_token`. `crypto` instead opens a per-transaction USDT deposit and returns a deposit `address` on a `network` (no `pay_token`); send `network` (for example `TRC20`) or omit it to use the operator default network. For `crypto`, `amount` is µUSDT and `currency` is ignored. The channels and crypto networks that are accepted follow the operator's System settings (the payment-method switches and the enabled crypto network set); a channel or network the operator has disabled is rejected with `422`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayinCreate"
              },
              "example": {
                "order_ref": "INV-2026-000123",
                "amount": 250000,
                "currency": "THB",
                "provider": "manual_slip"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The payin, wrapped in the ApiResponse envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/PayinView"
                        },
                        {
                          "$ref": "#/components/schemas/CryptoDeposit"
                        }
                      ]
                    },
                    "error": {
                      "type": "null"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "Payins"
        ],
        "summary": "List payins",
        "operationId": "listPayins",
        "description": "Newest first. Scope `payins:read`. `data` is a **bare array** of `PayinView`. Prefer `cursor` + `limit` for high-volume paging; `offset` remains for small pages.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/PayinStatus"
            }
          },
          {
            "name": "currency",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/Currency"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/PayinListOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/payins/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PayinId"
        },
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "get": {
        "tags": [
          "Payins"
        ],
        "summary": "Get a payin",
        "operationId": "getPayin",
        "description": "Scope `payins:read`. Returns the payin, or `404` if the id is unknown or belongs to another merchant.",
        "responses": {
          "200": {
            "$ref": "#/components/responses/PayinOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/payins/{id}/cancel": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PayinId"
        },
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "post": {
        "tags": [
          "Payins"
        ],
        "summary": "Cancel a payin",
        "operationId": "cancelPayin",
        "description": "Scope `payins:write`. **Idempotent** (`Idempotency-Key` required). Allowed only while the payin is `pending`; cancelling anything else is a `409 Conflict`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/PayinOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/payins/{id}/test-confirm": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PayinId"
        },
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "post": {
        "tags": [
          "Payins"
        ],
        "summary": "Confirm a sandbox payin",
        "operationId": "testConfirmPayin",
        "description": "Sandbox simulator for integration tests. Scope `payins:write`; the calling key and target payin must both be `test`. Requires `Idempotency-Key`. Transitions a pending test payin to `success`, emits a webhook with `livemode: false`, and never credits a balance or calls a live provider. A live key is rejected with `403`; an object from the other environment is not disclosed.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/PayinOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/payouts": {
      "parameters": [
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Send a payout",
        "operationId": "createPayout",
        "description": "Send money from the API key's environment balance to a counterparty account in that same environment. Scope `payouts:write`. **Idempotent**: an `Idempotency-Key` header is required. The gross amount is debited through a hold that is placed and captured atomically before the API returns `processing`; the captured hold is therefore no longer reported as `held`. A send is rejected with `422` before any money moves if available balance is insufficient. `order_ref` is unique per merchant and environment. Test requests use the sandbox simulator and never dispatch a live bank/provider command.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutCreate"
              },
              "example": {
                "order_ref": "PO-2026-000045",
                "amount": 500000,
                "dest_account_id": "aa11bb22-cc33-dd44-ee55-ff6677889900"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/PayoutOk"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "List payouts",
        "operationId": "listPayouts",
        "description": "Newest first. Scope `payouts:read`. `data` is a **bare array** of `PayoutView`. Prefer `cursor` + `limit` for high-volume paging; `offset` remains for small pages.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/PayoutStatus"
            }
          },
          {
            "name": "currency",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/Currency"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/PayoutListOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/payouts/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PayoutId"
        },
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Get a payout",
        "operationId": "getPayout",
        "description": "Scope `payouts:read`. Returns the payout, or `404` if the id is unknown or belongs to another merchant.",
        "responses": {
          "200": {
            "$ref": "#/components/responses/PayoutOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/payouts/{id}/cancel": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PayoutId"
        },
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Cancel a payout",
        "operationId": "cancelPayout",
        "description": "Scope `payouts:write`. **Idempotent** (`Idempotency-Key` required). Allowed while the payout is `pending` or `processing`. Refunds the gross debit, releases the daily limit reservation, and writes audit with actor `api_key`. Terminal replays return the existing view.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/PayoutOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/payouts/{id}/test-confirm": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PayoutId"
        },
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Confirm a sandbox payout",
        "operationId": "testConfirmPayout",
        "description": "Sandbox simulator for integration tests. Scope `payouts:write`; the calling key and target payout must both be `test`. Requires `Idempotency-Key`. Transitions a `processing` test payout to `success`, updates the linked `payout_attempts` row, emits `payout.succeeded` with `livemode: false`, and never calls a live bank/provider command. A live key is rejected with `403`; an object from the other environment is not disclosed.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/PayoutOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/settlements": {
      "parameters": [
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "post": {
        "tags": [
          "Settlements"
        ],
        "summary": "Create a settlement",
        "operationId": "createSettlement",
        "description": "Sweep all or part of the API key's environment balance to a verified counterparty account in that same environment. Scope `settlements:write`. **Idempotent**: an `Idempotency-Key` header is required. Test requests use the sandbox simulator and never dispatch a live bank/provider command.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SettlementCreate"
              },
              "example": {
                "dest_account_id": "aa11bb22-cc33-dd44-ee55-ff6677889900",
                "amount": 1000000,
                "client_operation_id": "01890f3e-2c80-7cc2-98c1-2e76c56cc2a9"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/SettlementOk"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "Settlements"
        ],
        "summary": "List settlements",
        "operationId": "listSettlements",
        "description": "Newest first in the API key's environment. Scope `settlements:read`. `data` is a bare array of `SettlementView`. Prefer `cursor` + `limit` for high-volume paging; `offset` remains for small pages.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "processing",
                "completed",
                "failed"
              ]
            }
          },
          {
            "name": "currency",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/Currency"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/SettlementListOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/settlements/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SettlementId"
        },
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "get": {
        "tags": [
          "Settlements"
        ],
        "summary": "Get a settlement",
        "operationId": "getSettlement",
        "description": "Scope `settlements:read`. Returns the settlement from the API key's environment, or `404` without disclosing an object from another merchant or environment.",
        "responses": {
          "200": {
            "$ref": "#/components/responses/SettlementOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/balance": {
      "parameters": [
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "get": {
        "tags": [
          "Balance"
        ],
        "summary": "Get balance",
        "operationId": "getBalance",
        "description": "Your spendable balance in the API key's environment, broken out per currency. Scope `balance:read`. `available` already excludes outstanding uncaptured holds; a current processing payout has captured its hold and is reflected by its gross balance debit instead. `data` is a **bare array**; every item carries `livemode`.",
        "responses": {
          "200": {
            "$ref": "#/components/responses/BalanceOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/sandbox/top-up": {
      "parameters": [
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "post": {
        "tags": [
          "Sandbox"
        ],
        "summary": "Top up a sandbox balance",
        "operationId": "topUpSandboxBalance",
        "description": "Test-only wallet funding for integration tests. Requires an active sk_test_ key with scope sandbox:topup and an Idempotency-Key (1–128 visible ASCII characters for this operation). Test keys issued before this scope was added must be regenerated to use it. The server fixes the merchant and test environment from the key; a live key is rejected with 403, and request fields cannot select a live wallet. amount_minor is a positive integer in the currency's minor units, at most 10000000 per request and divisible by its server-owned unit_step; the currency must be active. A balanced PostgreSQL journal and test-wallet credit commit atomically; the receipt carries livemode: false. Repeating the same key and raw request replays the receipt without another credit.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SandboxTopUpRequest"
              },
              "example": {
                "currency": "THB",
                "amount_minor": 10000
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/SandboxTopUpOk"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "A test-write emergency fence or money-authority containment gate prevents funding.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bank/verify": {
      "parameters": [
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "Verify bank account holder name",
        "operationId": "verifyBankAccount",
        "description": "Live-only lookup of the registered holder name for a Thai bank account. Scope `bank:verify`. Only `sk_live_` keys may reach the shared production cache or configured upstream provider; `sk_test_` is rejected with `403` before either path. A cache hit does not call the upstream provider. This endpoint does **not** set `is_verified` on a customer record.",
        "parameters": [
          {
            "name": "bank_code",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "example": "KBANK"
            },
            "description": "Thai bank code (uppercased on lookup)."
          },
          {
            "name": "account_no",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{6,20}$",
              "example": "1234567890"
            },
            "description": "Destination account number (6–20 digits)."
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/BankVerifyOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Bank name verification is disabled on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "example": {
                  "success": false,
                  "data": null,
                  "error": "bank name verification is not enabled on this gateway; contact support to enable it",
                  "code": "BANK_VERIFY_DISABLED"
                }
              }
            }
          }
        }
      }
    },
    "/v1/slip/verify": {
      "parameters": [
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "post": {
        "tags": [
          "Payins"
        ],
        "summary": "Verify a bank transfer slip from QR data",
        "operationId": "verifySlipQr",
        "description": "Live-only lookup of a Thai bank slip from the QR payload printed on the transfer receipt. Scope `slip:verify`. **Idempotent**: an `Idempotency-Key` header is required; reuse the same key for every retry. Only `sk_live_` keys may reach the shared production cache or configured upstream provider; `sk_test_` is rejected with `403` before either path. A cache hit within 24h does not call the upstream provider. `data.success` is the business outcome — HTTP `200` is returned for both verified and rejected slips. Amounts are integer minor units (satang for THB). This endpoint does **not** credit a payin or move money.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SlipVerifyRequest"
              },
              "example": {
                "qr_data": "0046000600000101030060225N0067683862100760851590365102TH9104476C"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/SlipVerifyOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Slip verification is disabled on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "example": {
                  "success": false,
                  "data": null,
                  "error": "slip verification is not enabled on this gateway; contact support to enable it",
                  "code": "SLIP_VERIFY_DISABLED"
                }
              }
            }
          }
        }
      }
    },
    "/v1/slip/verify/{ref}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "get": {
        "tags": [
          "Payins"
        ],
        "summary": "Lookup a cached slip verification by transaction reference",
        "operationId": "getSlipByTransactionRef",
        "description": "Returns the most recent cached slip verification for a provider transaction reference. Scope `slip:verify`. Live-only (`sk_test_` → `403`). Does not call the upstream provider.",
        "parameters": [
          {
            "name": "ref",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "67683862100760851590365"
            },
            "description": "Provider transaction reference from a prior successful slip verification."
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/SlipVerifyOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/customers": {
      "parameters": [
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "post": {
        "tags": [
          "Customers"
        ],
        "summary": "Create a customer",
        "operationId": "createCustomer",
        "description": "Register a counterparty bank account in the API key's environment. Scope `customers:write`. **Idempotent**: an `Idempotency-Key` header is required. The pair `(bank_code, account_no)` must be unique per merchant and environment; test and live records are isolated. Sandbox customers are auto-verified for simulator-only settlement use without calling the live bank verifier; new live customers remain unverified until the dashboard finance flow verifies them.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerCreate"
              },
              "example": {
                "holder_name": "Somchai Jaidee",
                "bank_code": "KBANK",
                "account_no": "1234567890",
                "currency": "THB"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/CustomerOk"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "List customers",
        "operationId": "listCustomers",
        "description": "Newest first in the API key's environment. Scope `customers:read`. `data` is a **bare array** of `CustomerView`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "blocked",
                "archived"
              ]
            }
          },
          {
            "name": "bank_code",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/CustomerListOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/customers/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerId"
        },
        {
          "$ref": "#/components/parameters/HmacSignature"
        },
        {
          "$ref": "#/components/parameters/HmacSignatureTimestamp"
        }
      ],
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "Get a customer",
        "operationId": "getCustomer",
        "description": "Scope `customers:read`. Returns the customer from the API key's environment, or `404` without disclosing an object from another merchant or environment.",
        "responses": {
          "200": {
            "$ref": "#/components/responses/CustomerOk"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "tags": [
          "Customers"
        ],
        "summary": "Update a customer",
        "operationId": "updateCustomer",
        "description": "Scope `customers:write`. Updates only a customer in the API key's environment; an id from the other environment is not disclosed. Body carries only the fields you want to change. `trust_score` and `is_verified` are read-only over `/v1` — they move only through the live dashboard finance flow.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerUpdate"
              },
              "example": {
                "status": "blocked"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/CustomerOk"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sk_test_... | sk_live_...",
        "description": "Secret API key sent as a Bearer token on every request. The prefix selects the server-owned financial environment. Financial resources, counterparty accounts, queued webhook jobs, event identities, and delivery dedupe are isolated by that environment. Webhook endpoint registrations and signing secrets are isolated by environment. The merchant IP allowlist and published API Base URL remain shared. Webhook bodies expose the server-owned livemode flag. HMAC request signing, when enabled for a key in either environment, is an additional check and never replaces this Bearer credential. A missing, malformed, unknown, or key not in status `active` is rejected with 401. A key lacking the route's scope, or a key in the `test` environment attempting a provider operation reserved for `live`, is rejected with 403."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "Opaque unique value for one logical operation. UUID v7 is recommended for new values but not required; reuse the same key for every retry. The namespace is `(merchant, environment, key)`, so the same value may be used independently in test and live. Request identity is `SHA256(POST + \"\\n\" + ROUTE_PATH + \"\\n\" + RAW_BODY)`: `ROUTE_PATH` is the already-encoded route path visible to the `/v1` router after Axum strips the `/v1` mount prefix (for example, client path `/v1/payins` hashes `/payins`); query excluded; do not decode, normalize, or sort. The raw body is the exact request body bytes (do not parse or reserialize). This layer applies only to `POST`; other methods bypass it. Same key + same route path and raw body replays the original 2xx response with header `idempotent-replay: true`. Same key + different route path or raw body → 409.",
        "schema": {
          "type": "string",
          "maxLength": 255,
          "example": "01890f3e-2c80-7cc2-98c1-2e76c56cc2a8"
        }
      },
      "HmacSignature": {
        "name": "X-Signature",
        "in": "header",
        "required": false,
        "description": "Optional unless HMAC request signing is enabled for the authenticated API key in either environment. When enabled, send the lowercase hexadecimal HMAC-SHA256 of `METHOD\\nPATH_AND_QUERY\\nTIMESTAMP\\nIDEMPOTENCY_KEY\\nBODY` using the reusable `hs_...` signing secret revealed once. `METHOD` is the exact method token sent. `PATH_AND_QUERY` is the exact raw, already-encoded path and query received by the server; do not decode, normalize, or sort it. `IDEMPOTENCY_KEY` is the exact header value, or an empty string when absent. `BODY` is the raw request body bytes, or empty bytes when absent.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$",
          "example": "53d0ef1c531232448fd78ecb63a0fceeda47f96dae407ce13ce31fc9e43ec2d8"
        }
      },
      "HmacSignatureTimestamp": {
        "name": "X-Signature-Timestamp",
        "in": "header",
        "required": false,
        "description": "Optional unless HMAC request signing is enabled for the authenticated API key in either environment. When enabled, send canonical base-10 Unix seconds within ±300 seconds of server time and sign the same value. For a retry of the same logical operation, reuse the original `Idempotency-Key` but create a fresh timestamp and signature.",
        "schema": {
          "type": "string",
          "pattern": "^(0|[1-9][0-9]*)$",
          "example": "1700000000"
        }
      },
      "PayinId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Payin UUID.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "PayoutId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Payout UUID.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "SettlementId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Settlement UUID.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "CustomerId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Customer (counterparty account) UUID.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Page size.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 50
        }
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "required": false,
        "description": "Records to skip for pagination.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Stable keyset cursor from the previous page's last row: `{RFC3339}_{uuid}` built from that row's `created_at` and `id`. Mutually exclusive with a non-zero `offset`. Ordering is `(created_at DESC, id DESC)`.",
        "schema": {
          "type": "string"
        },
        "examples": [
          "2026-08-22T10:15:30Z_aa11bb22-cc33-dd44-ee55-ff6677889900"
        ]
      }
    },
    "schemas": {
      "MinorAmount": {
        "type": "integer",
        "format": "int64",
        "minimum": 0,
        "description": "A nonnegative integer count of the accompanying currency's minor units. Serialized as a JSON number, never a float. The server-owned currency profile defines the storage scale and required unit-step alignment; for THB, 250000 means 2,500.00 THB.",
        "examples": [
          250000
        ]
      },
      "Currency": {
        "type": "string",
        "description": "Server-owned 3–8 character currency code. The current registry contains THB, MMK, CNY, EUR, GBP, IDR, INR, JPY, KHR, KRW, LAK, PHP, USD, VND, AUD, TWD, HKD, and USDT. A catalog row can remain readable after deactivation. Each new write separately enforces its applicable readiness: an active currency for new financial identities or movement, an explicit merchant fee profile for fee-taking flows, and an enabled collection account for live payins. No implicit FX is performed.",
        "examples": [
          "THB",
          "JPY",
          "USD",
          "USDT"
        ]
      },
      "FinancialEnvironment": {
        "type": "string",
        "enum": [
          "test",
          "live"
        ],
        "description": "Server-owned financial namespace selected by the API-key prefix; never accepted from a request body."
      },
      "PayinStatus": {
        "type": "string",
        "enum": [
          "pending",
          "success",
          "expired",
          "cancelled",
          "failed"
        ],
        "description": "Payin lifecycle state. `success`, `expired`, `cancelled`, `failed` are terminal."
      },
      "PayoutStatus": {
        "type": "string",
        "enum": [
          "pending",
          "processing",
          "success",
          "failed",
          "cancelled"
        ],
        "description": "Payout lifecycle state. `success`, `failed`, `cancelled` are terminal."
      },
      "PayinCreate": {
        "type": "object",
        "required": [
          "order_ref",
          "amount"
        ],
        "properties": {
          "order_ref": {
            "type": "string",
            "maxLength": 100,
            "description": "Your unique reference for this payment. Unique per merchant and environment; a duplicate in the same environment is a 409.",
            "examples": [
              "INV-2026-000123"
            ]
          },
          "amount": {
            "$ref": "#/components/schemas/MinorAmount"
          },
          "currency": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Currency"
              }
            ],
            "default": "THB",
            "description": "Defaults to THB. Live creation succeeds only when the requested currency is active, the merchant has an explicit fee profile, and an enabled collection account exists for the requested currency/provider; the initial live manual-slip rollout is THB. Sandbox creation can exercise other active catalog currencies without exposing a live collection destination."
          },
          "provider": {
            "type": "string",
            "default": "manual_slip",
            "description": "Payin provider. `manual_slip` is the currently implemented provider; live admission also requires an enabled merchant/currency/provider collection account.",
            "examples": [
              "manual_slip"
            ]
          },
          "receiving_type": {
            "type": "string",
            "enum": [
              "p2p",
              "bank_transfer",
              "qr"
            ],
            "examples": [
              "p2p"
            ]
          },
          "channel": {
            "type": "string",
            "enum": [
              "bank",
              "qrcode",
              "crypto"
            ],
            "default": "bank",
            "examples": [
              "bank"
            ],
            "description": "Selects the payment rail as a shortcut for `provider`/`receiving_type`; when set it overrides them. `bank` (the default when omitted) is a bank transfer and `qrcode` (alias `qr`) is a PromptPay QR — both return a `PayinView` with a hosted-pay `pay_token`. `crypto` instead opens a per-transaction USDT deposit and returns a deposit address on a `network` (no `pay_token`). The channels that are accepted follow the operator's System settings; a disabled channel is rejected with `422`."
          },
          "network": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "TRC20",
              "SOLANA",
              "BEP20",
              "ERC20",
              null
            ],
            "examples": [
              "TRC20"
            ],
            "description": "Crypto network for `channel = crypto` (`TRC20`, `SOLANA`, `BEP20`, or `ERC20`); ignored for other channels. Omit it to use the operator's default network. Must be one of the operator's enabled networks, otherwise the request is a `422`. For `crypto`, `amount` is µUSDT and `currency` is ignored."
          },
          "client_operation_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Optional client-side operation UUID for extra dedupe. UUID v7 is recommended for new values; historical UUID versions already stored are still accepted on read."
          },
          "counterparty_account_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Optional counterparty account in the API key's environment this payin is expected from. The sandbox hosted-pay surface never exposes a live collection destination."
          },
          "redirect_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "maxLength": 2048,
            "description": "Optional HTTPS redirect after hosted-pay success or expiry."
          },
          "pay_theme": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "classic",
              "light",
              "dark",
              "breeze",
              "linen",
              "royal",
              "grid",
              "coral",
              "onyx",
              "aurora",
              "terra",
              "leaf",
              "neon",
              "forest-green",
              "choco-cosmos",
              "uefa",
              "smoky-black",
              "safety-orange",
              "deep-green",
              "purple-dark",
              "pink",
              "blue-dark",
              "lime-green",
              "sky-fun",
              "modern",
              "modern-blue",
              "modern-teal",
              "modern-emerald",
              "modern-violet",
              "modern-rose",
              "modern-amber",
              "modern-slate",
              null
            ],
            "description": "Optional hosted-pay theme."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2000,
            "description": "Optional merchant memo."
          }
        }
      },
      "PayoutCreate": {
        "type": "object",
        "required": [
          "order_ref",
          "amount",
          "dest_account_id"
        ],
        "properties": {
          "order_ref": {
            "type": "string",
            "maxLength": 100,
            "description": "Your unique reference for this payout. Unique per merchant and environment; a duplicate in the same environment is a 409.",
            "examples": [
              "PO-2026-000045"
            ]
          },
          "amount": {
            "$ref": "#/components/schemas/MinorAmount"
          },
          "dest_account_id": {
            "type": "string",
            "format": "uuid",
            "description": "UUID of a provisioned counterparty account (the destination bank account). No inline bank details are sent.",
            "examples": [
              "aa11bb22-cc33-dd44-ee55-ff6677889900"
            ]
          },
          "client_operation_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Optional client-side operation UUID for extra dedupe. UUID v7 is recommended for new values; historical UUID versions already stored are still accepted on read."
          }
        }
      },
      "SettlementCreate": {
        "type": "object",
        "required": [
          "dest_account_id"
        ],
        "properties": {
          "dest_account_id": {
            "type": "string",
            "format": "uuid",
            "description": "UUID of a verified counterparty account in the API key's environment."
          },
          "amount": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/MinorAmount"
              },
              {
                "type": "null"
              }
            ],
            "description": "Optional gross amount in the destination account currency's minor units. Omit or send null to sweep the full currently available balance for that currency in the key's environment."
          },
          "client_operation_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Optional client-side operation UUID for extra dedupe. UUID v7 is recommended for new values; historical UUID versions already stored are still accepted on read."
          }
        }
      },
      "PayinView": {
        "type": "object",
        "required": [
          "id",
          "order_ref",
          "currency",
          "amount",
          "fee",
          "net",
          "provider",
          "receiving_type",
          "status",
          "created_at",
          "succeeded_at",
          "pay_token",
          "expires_at",
          "env",
          "livemode",
          "redirect_url",
          "pay_theme",
          "notes",
          "requested_amount"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "order_ref": {
            "type": "string",
            "examples": [
              "INV-2026-000123"
            ]
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "amount": {
            "$ref": "#/components/schemas/MinorAmount"
          },
          "fee": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MinorAmount"
              }
            ],
            "description": "Gateway fee in the payin currency's minor units, computed from the merchant's currency-scoped tariff."
          },
          "net": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MinorAmount"
              }
            ],
            "description": "What lands in your balance: net = amount − fee."
          },
          "provider": {
            "type": "string",
            "examples": [
              "manual_slip"
            ]
          },
          "receiving_type": {
            "type": "string",
            "enum": [
              "p2p",
              "bank_transfer",
              "qr"
            ],
            "examples": [
              "p2p"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/PayinStatus"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "succeeded_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set once the payin reaches `success`."
          },
          "pay_token": {
            "type": "string",
            "description": "Opaque hosted-pay handle. The public hosted-pay surface resolves live payins only; use `test-confirm` for sandbox payins."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "env": {
            "$ref": "#/components/schemas/FinancialEnvironment"
          },
          "livemode": {
            "type": "boolean",
            "description": "True exactly when `env` is `live`."
          },
          "redirect_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "pay_theme": {
            "type": [
              "string",
              "null"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "requested_amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MinorAmount"
              }
            ],
            "description": "Original requested amount, retained after a reviewed slip adjustment."
          }
        }
      },
      "SettlementView": {
        "type": "object",
        "required": [
          "id",
          "currency",
          "amount",
          "fee",
          "net",
          "dest_account_id",
          "status",
          "created_at",
          "completed_at",
          "livemode"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "amount": {
            "$ref": "#/components/schemas/MinorAmount"
          },
          "fee": {
            "$ref": "#/components/schemas/MinorAmount"
          },
          "net": {
            "$ref": "#/components/schemas/MinorAmount"
          },
          "dest_account_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "livemode": {
            "type": "boolean",
            "description": "True only for a settlement in the live financial namespace."
          }
        }
      },
      "PayoutView": {
        "type": "object",
        "required": [
          "id",
          "order_ref",
          "currency",
          "amount",
          "fee",
          "net",
          "dest_account_id",
          "status",
          "created_at",
          "processed_at",
          "livemode"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "order_ref": {
            "type": "string",
            "examples": [
              "PO-2026-000045"
            ]
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "amount": {
            "$ref": "#/components/schemas/MinorAmount"
          },
          "fee": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MinorAmount"
              }
            ],
            "description": "Gateway fee in the payout currency's minor units, included inside the gross amount and computed from the merchant's currency-scoped tariff."
          },
          "net": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MinorAmount"
              }
            ],
            "description": "What reaches the destination: net = amount − fee."
          },
          "dest_account_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "$ref": "#/components/schemas/PayoutStatus"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "processed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set once the payout is processed (success/failed)."
          },
          "livemode": {
            "type": "boolean",
            "description": "True only for a payout in the live financial namespace."
          }
        }
      },
      "SandboxTopUpRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "currency",
          "amount_minor"
        ],
        "properties": {
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "amount_minor": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "maximum": 10000000,
            "description": "Positive integer amount in the currency's minor units; must be divisible by its server-owned unit_step."
          }
        }
      },
      "SandboxTopUpReceipt": {
        "type": "object",
        "required": [
          "operation_id",
          "journal_id",
          "currency",
          "amount_minor",
          "balance_before",
          "balance_after",
          "livemode"
        ],
        "properties": {
          "operation_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifier of the test-wallet credit operation."
          },
          "journal_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifier of the balanced PostgreSQL journal."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "amount_minor": {
            "type": "integer",
            "format": "int64"
          },
          "balance_before": {
            "type": "integer",
            "format": "int64",
            "description": "Test-wallet balance before this credit, in minor units."
          },
          "balance_after": {
            "type": "integer",
            "format": "int64",
            "description": "Test-wallet balance after this credit, in minor units."
          },
          "livemode": {
            "type": "boolean",
            "const": false
          }
        }
      },
      "Balance": {
        "type": "object",
        "required": [
          "currency",
          "posted",
          "pending",
          "pending_credits",
          "available",
          "held",
          "livemode",
          "authority_epoch"
        ],
        "properties": {
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "posted": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MinorAmount"
              }
            ],
            "description": "Posted merchant liability in the row currency's minor units. Under TigerBeetle authority this is credits_posted minus debits_posted on the merchant-payable account; under PostgreSQL shadow it matches the stored wallet balance before holds."
          },
          "pending": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MinorAmount"
              }
            ],
            "description": "Pending debits in the row currency's minor units (TigerBeetle debits_pending or, in PostgreSQL shadow, the same total as `held`)."
          },
          "pending_credits": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MinorAmount"
              }
            ],
            "description": "Pending credits on the merchant-payable account. Never spendable; always excluded from `available`. Zero in PostgreSQL shadow mode."
          },
          "available": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MinorAmount"
              }
            ],
            "description": "Spendable now, in the row currency's minor units. Under TigerBeetle authority this is posted minus pending debits; under PostgreSQL shadow it is the stored balance minus outstanding uncaptured holds in the same environment and currency."
          },
          "held": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MinorAmount"
              }
            ],
            "description": "Compatibility alias for pending debits / outstanding uncaptured reservations in the row currency's minor units. A current processing payout captures its hold atomically and is represented by its gross balance debit, not by this field."
          },
          "livemode": {
            "type": "boolean",
            "description": "True only for a balance in the live financial namespace."
          },
          "authority_epoch": {
            "type": "integer",
            "format": "int64",
            "description": "Current balance-authority epoch for this environment. Clients must not infer spend authority from stale projections when this epoch advances."
          }
        }
      },
      "CustomerStatus": {
        "type": "string",
        "enum": [
          "active",
          "blocked",
          "archived"
        ],
        "description": "Counterparty account lifecycle state."
      },
      "CustomerCreate": {
        "type": "object",
        "required": [
          "holder_name",
          "bank_code",
          "account_no"
        ],
        "properties": {
          "holder_name": {
            "type": "string",
            "description": "Bank account holder name.",
            "examples": [
              "Somchai Jaidee"
            ]
          },
          "bank_code": {
            "type": "string",
            "description": "Bank or rail-institution code, uppercased on store (e.g. KBANK). The Bank codes page lists the supported Thai-bank catalog used by the current THB tooling and holder-name verification rail.",
            "examples": [
              "KBANK"
            ]
          },
          "account_no": {
            "type": "string",
            "minLength": 6,
            "maxLength": 20,
            "pattern": "^[0-9]+$",
            "description": "Destination account number (6–20 digits).",
            "examples": [
              "1234567890"
            ]
          },
          "currency": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Currency"
              }
            ],
            "default": "THB",
            "description": "Defaults to THB."
          }
        }
      },
      "CustomerUpdate": {
        "type": "object",
        "properties": {
          "holder_name": {
            "type": "string",
            "description": "New bank account holder name."
          },
          "status": {
            "$ref": "#/components/schemas/CustomerStatus",
            "description": "Set `blocked` to stop new payouts/settlements; `archived` to retire the account."
          }
        }
      },
      "BankVerifyView": {
        "type": "object",
        "required": [
          "bank_code",
          "account_no",
          "holder_name",
          "cached"
        ],
        "properties": {
          "bank_code": {
            "type": "string",
            "examples": [
              "KBANK"
            ]
          },
          "account_no": {
            "type": "string",
            "examples": [
              "1234567890"
            ]
          },
          "holder_name": {
            "type": "string",
            "examples": [
              "Somchai Jaidee"
            ]
          },
          "cached": {
            "type": "boolean",
            "description": "True when the name was served from the global cache without calling the upstream provider."
          },
          "verified_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When this cache entry was last refreshed."
          }
        }
      },
      "SlipVerifyRequest": {
        "type": "object",
        "required": [
          "qr_data"
        ],
        "properties": {
          "qr_data": {
            "type": "string",
            "description": "QR payload from a Thai bank transfer slip.",
            "examples": [
              "0046000600000101030060225N0067683862100760851590365102TH9104476C"
            ]
          }
        }
      },
      "SlipVerifyView": {
        "type": "object",
        "required": [
          "qr_data",
          "success",
          "cached"
        ],
        "properties": {
          "qr_data": {
            "type": "string"
          },
          "success": {
            "type": "boolean",
            "description": "True when the upstream provider accepted the slip QR."
          },
          "cached": {
            "type": "boolean",
            "description": "True when served from the global cache without calling the upstream provider."
          },
          "verified_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "message": {
            "type": "string",
            "nullable": true
          },
          "error_detail": {
            "type": "string",
            "nullable": true
          },
          "transaction_ref": {
            "type": "string",
            "nullable": true
          },
          "transaction_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "description": "Transfer amount in minor units (satang for THB)."
          },
          "currency": {
            "type": "string",
            "nullable": true,
            "examples": [
              "THB"
            ]
          },
          "from_bank_code": {
            "type": "string",
            "nullable": true,
            "examples": [
              "KTB"
            ]
          },
          "from_account_no": {
            "type": "string",
            "nullable": true
          },
          "from_account_name": {
            "type": "string",
            "nullable": true
          },
          "to_bank_code": {
            "type": "string",
            "nullable": true,
            "examples": [
              "SCB"
            ]
          },
          "to_account_no": {
            "type": "string",
            "nullable": true
          },
          "to_account_name": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "CustomerView": {
        "type": "object",
        "required": [
          "id",
          "holder_name",
          "bank_code",
          "account_no",
          "currency",
          "trust_score",
          "is_verified",
          "status",
          "payin_total",
          "payin_count",
          "payin_expired_count",
          "payout_total",
          "payout_count",
          "star_score_tenths",
          "created_at",
          "updated_at",
          "livemode",
          "payin_adjusted_count",
          "payin_adjusted_score_tenths",
          "payin_expired_score_tenths",
          "fast_score_tenths",
          "exp_score_tenths",
          "accuracy_score_tenths"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "holder_name": {
            "type": "string",
            "examples": [
              "Somchai Jaidee"
            ]
          },
          "bank_code": {
            "type": "string",
            "examples": [
              "KBANK"
            ]
          },
          "account_no": {
            "type": "string",
            "examples": [
              "1234567890"
            ]
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "trust_score": {
            "type": "integer",
            "minimum": 0,
            "maximum": 5,
            "description": "Risk/trust rating (0–5). Read-only over `/v1`."
          },
          "is_verified": {
            "type": "boolean",
            "description": "Whether the account is usable for settlements. Read-only over `/v1`; sandbox records are auto-verified without a live provider call, while live records require the dashboard finance flow."
          },
          "status": {
            "$ref": "#/components/schemas/CustomerStatus"
          },
          "payin_total": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Successful payin volume in integer minor units for this account, scoped to the same merchant, environment, and currency."
          },
          "payin_count": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Number of successful payins for this account in the same financial namespace."
          },
          "payin_expired_count": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Number of expired unpaid deposits in the same financial namespace."
          },
          "payout_total": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Successful payout volume in integer minor units for this account, scoped to the same merchant, environment, and currency."
          },
          "payout_count": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Number of successful payouts for this account in the same financial namespace."
          },
          "star_score_tenths": {
            "type": "integer",
            "format": "int64",
            "description": "Automatic customer star in tenths: `(payin_count - payin_adjusted_count) * 10 + payin_expired_score_tenths + payin_adjusted_score_tenths`. Exact paid deposits earn +1.0 stars. P2P expiries and adjusted deposits use saved deductions; other expired deposits contribute -0.5 stars. Later system policy changes do not change past scores. Other statuses are neutral. Derived on read, can be negative, and is separate from the manually assigned `trust_score`.",
            "examples": [
              30
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "livemode": {
            "type": "boolean",
            "description": "True only for a counterparty account in the live financial namespace."
          },
          "payin_adjusted_count": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Successful deposits whose transferred amount differed from the requested amount, scoped to the same merchant, environment, and currency."
          },
          "payin_adjusted_score_tenths": {
            "type": "integer",
            "format": "int64",
            "maximum": 0,
            "description": "Sum of the saved star deductions for successful adjusted deposits, in tenths. This is zero or negative and is isolated by merchant, environment, and currency."
          },
          "payin_expired_score_tenths": {
            "type": "integer",
            "format": "int64",
            "maximum": 0,
            "description": "Sum of expiry deductions in tenths. P2P deposits use the score saved at expiry; other channels retain -0.5 stars per expired deposit. New P2P expiries with an uploaded slip or a credited payment are neutral. Zero or negative, isolated by merchant, environment, and currency."
          },
          "fast_score_tenths": {
            "type": "integer",
            "format": "int64",
            "description": "Independent P2P speed score in tenths. Each confirmed payment contributes +10 for timely evidence or -10 for late evidence. The default window is 5 minutes from creation and is configurable. Missing reliable payment time is neutral. Never sum this with the other score dimensions."
          },
          "exp_score_tenths": {
            "type": "integer",
            "format": "int64",
            "description": "Independent P2P payment-completion score in tenths. Each successful payment contributes +10; an unpaid expiry without recorded payment evidence contributes -10. Pending or unconfirmed payments with evidence are neutral. Never sum this with the other score dimensions."
          },
          "accuracy_score_tenths": {
            "type": "integer",
            "format": "int64",
            "description": "Independent P2P amount-accuracy score in tenths. Each successful exact-amount payment contributes +10; a successful reviewed amount correction contributes -10. Unpaid or pending deposits are neutral. Never sum this with the other score dimensions."
          }
        }
      },
      "ApiError": {
        "type": "object",
        "required": [
          "success",
          "data",
          "error",
          "code"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "data": {
            "type": "null"
          },
          "error": {
            "type": "string",
            "description": "Human-readable message to log.",
            "examples": [
              "the API key is missing the scope this endpoint requires"
            ]
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code. Branch on this, not on the message text.",
            "examples": [
              "API_KEY_SCOPE_MISSING"
            ]
          }
        }
      },
      "CryptoDeposit": {
        "type": "object",
        "required": [
          "topup_request_id",
          "deposit_address_id",
          "network",
          "address",
          "reserved_atomic",
          "expires_at",
          "pay_token"
        ],
        "properties": {
          "topup_request_id": {
            "type": "string",
            "format": "uuid",
            "description": "The created USDT deposit request id."
          },
          "deposit_address_id": {
            "type": "string",
            "format": "uuid",
            "description": "Id of the freshly minted deposit address."
          },
          "network": {
            "type": "string",
            "enum": [
              "TRC20",
              "SOLANA",
              "BEP20",
              "ERC20"
            ],
            "description": "Crypto network the deposit address is on."
          },
          "address": {
            "type": "string",
            "description": "On-chain USDT deposit address; send `reserved_atomic` µUSDT to it."
          },
          "reserved_atomic": {
            "type": "integer",
            "format": "int64",
            "description": "Amount to send, in µUSDT."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the deposit window closes."
          },
          "pay_token": {
            "type": "string",
            "description": "Opaque hosted-pay handle for the crypto deposit page."
          }
        }
      }
    },
    "responses": {
      "PayinOk": {
        "description": "The payin, wrapped in the ApiResponse envelope.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "success",
                "data",
                "error"
              ],
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": true
                },
                "data": {
                  "$ref": "#/components/schemas/PayinView"
                },
                "error": {
                  "type": "null"
                }
              }
            }
          }
        }
      },
      "PayinListOk": {
        "description": "A bare array of payins, wrapped in the ApiResponse envelope.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "success",
                "data",
                "error"
              ],
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": true
                },
                "data": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PayinView"
                  }
                },
                "error": {
                  "type": "null"
                }
              }
            }
          }
        }
      },
      "PayoutOk": {
        "description": "The payout, wrapped in the ApiResponse envelope.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "success",
                "data",
                "error"
              ],
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": true
                },
                "data": {
                  "$ref": "#/components/schemas/PayoutView"
                },
                "error": {
                  "type": "null"
                }
              }
            }
          }
        }
      },
      "PayoutListOk": {
        "description": "A bare array of payouts, wrapped in the ApiResponse envelope.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "success",
                "data",
                "error"
              ],
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": true
                },
                "data": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PayoutView"
                  }
                },
                "error": {
                  "type": "null"
                }
              }
            }
          }
        }
      },
      "SettlementOk": {
        "description": "The settlement, wrapped in the ApiResponse envelope.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "success",
                "data",
                "error"
              ],
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": true
                },
                "data": {
                  "$ref": "#/components/schemas/SettlementView"
                },
                "error": {
                  "type": "null"
                }
              }
            }
          }
        }
      },
      "SettlementListOk": {
        "description": "A bare array of settlements, wrapped in the ApiResponse envelope.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "success",
                "data",
                "error"
              ],
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": true
                },
                "data": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SettlementView"
                  }
                },
                "error": {
                  "type": "null"
                }
              }
            }
          }
        }
      },
      "BalanceOk": {
        "description": "Per-currency balances, wrapped in the ApiResponse envelope.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "success",
                "data",
                "error"
              ],
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": true
                },
                "data": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Balance"
                  }
                },
                "error": {
                  "type": "null"
                }
              }
            },
            "example": {
              "success": true,
              "data": [
                {
                  "currency": "THB",
                  "posted": 1750000,
                  "pending": 0,
                  "pending_credits": 0,
                  "available": 1750000,
                  "held": 0,
                  "livemode": false,
                  "authority_epoch": 0
                }
              ],
              "error": null
            }
          }
        }
      },
      "SandboxTopUpOk": {
        "description": "Test-wallet credit receipt wrapped in the ApiResponse envelope.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "success",
                "data",
                "error"
              ],
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": true
                },
                "data": {
                  "$ref": "#/components/schemas/SandboxTopUpReceipt"
                },
                "error": {
                  "type": "null"
                }
              }
            },
            "example": {
              "success": true,
              "data": {
                "operation_id": "019a0000-0000-7000-8000-000000000001",
                "journal_id": "019a0000-0000-7000-8000-000000000002",
                "currency": "THB",
                "amount_minor": 10000,
                "balance_before": 0,
                "balance_after": 10000,
                "livemode": false
              },
              "error": null
            }
          }
        }
      },
      "BankVerifyOk": {
        "description": "Bank account holder name lookup result.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "success",
                "data",
                "error"
              ],
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": true
                },
                "data": {
                  "$ref": "#/components/schemas/BankVerifyView"
                },
                "error": {
                  "type": "null"
                }
              }
            },
            "example": {
              "success": true,
              "data": {
                "bank_code": "KBANK",
                "account_no": "1234567890",
                "holder_name": "Somchai Jaidee",
                "cached": false,
                "verified_at": "2026-08-20T00:10:00Z"
              },
              "error": null
            }
          }
        }
      },
      "SlipVerifyOk": {
        "description": "Slip QR verification result. `data.success` is the business outcome.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "success",
                "data",
                "error"
              ],
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": true
                },
                "data": {
                  "$ref": "#/components/schemas/SlipVerifyView"
                },
                "error": {
                  "type": "null"
                }
              }
            },
            "example": {
              "success": true,
              "data": {
                "qr_data": "0046000600000101030060225N0067683862100760851590365102TH9104476C",
                "success": true,
                "cached": false,
                "verified_at": "2026-08-20T00:10:00Z",
                "transaction_ref": "67683862100760851590365",
                "transaction_at": "2024-01-15T07:30:00Z",
                "amount": 150000,
                "currency": "THB",
                "from_bank_code": "KTB",
                "from_account_no": "1234567890",
                "to_bank_code": "SCB",
                "to_account_no": "9876543210"
              },
              "error": null
            }
          }
        }
      },
      "CustomerOk": {
        "description": "The customer, wrapped in the ApiResponse envelope.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "success",
                "data",
                "error"
              ],
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": true
                },
                "data": {
                  "$ref": "#/components/schemas/CustomerView"
                },
                "error": {
                  "type": "null"
                }
              }
            },
            "example": {
              "success": true,
              "data": {
                "id": "aa11bb22-cc33-dd44-ee55-ff6677889900",
                "holder_name": "Somchai Jaidee",
                "bank_code": "KBANK",
                "account_no": "1234567890",
                "currency": "THB",
                "trust_score": 0,
                "is_verified": false,
                "status": "active",
                "payin_total": 0,
                "payin_count": 0,
                "payin_expired_count": 0,
                "payout_total": 0,
                "payout_count": 0,
                "star_score_tenths": 0,
                "created_at": "2026-08-19T11:40:00Z",
                "updated_at": "2026-08-19T11:40:00Z",
                "livemode": true
              },
              "error": null
            }
          }
        }
      },
      "CustomerListOk": {
        "description": "A bare array of customers, wrapped in the ApiResponse envelope.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "success",
                "data",
                "error"
              ],
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": true
                },
                "data": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CustomerView"
                  }
                },
                "error": {
                  "type": "null"
                }
              }
            }
          }
        }
      },
      "BadRequest": {
        "description": "Malformed request identity or amount — including an unknown/inactive currency for new admission, non-positive or unit-step-misaligned money, an invalid fee split, or malformed customer fields.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "success": false,
              "data": null,
              "error": "amount must be a multiple of the currency unit step",
              "code": "AMOUNT_NOT_DIVISIBLE"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Bad credentials or request signing — missing/malformed Authorization header, unknown key, key not in status `active`, or a missing, invalid, or stale HMAC signature for a key that has signing enabled.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "examples": {
              "api_key_invalid": {
                "description": "The Bearer API key is unknown.",
                "value": {
                  "success": false,
                  "data": null,
                  "error": "the API key provided is not valid",
                  "code": "API_KEY_INVALID"
                }
              },
              "signature_missing": {
                "description": "The authenticated API key has HMAC signing enabled, but X-Signature is absent.",
                "value": {
                  "success": false,
                  "data": null,
                  "error": "this API key requires request signing; send X-Signature and X-Signature-Timestamp",
                  "code": "SIGNATURE_MISSING"
                }
              },
              "signature_invalid": {
                "description": "The supplied signature is malformed or does not match the exact request bytes.",
                "value": {
                  "success": false,
                  "data": null,
                  "error": "the request signature does not match",
                  "code": "SIGNATURE_INVALID"
                }
              },
              "signature_timestamp_invalid": {
                "description": "X-Signature-Timestamp is missing, malformed, duplicated, or outside the accepted ±300 second window.",
                "value": {
                  "success": false,
                  "data": null,
                  "error": "the X-Signature-Timestamp header is missing, malformed, or outside the accepted window",
                  "code": "SIGNATURE_TIMESTAMP_INVALID"
                }
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "Key lacks the required scope, or the client IP is not in the merchant allowlist.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "success": false,
              "data": null,
              "error": "the API key is missing the scope this endpoint requires",
              "code": "API_KEY_SCOPE_MISSING"
            }
          }
        }
      },
      "NotFound": {
        "description": "The id is unknown or belongs to another merchant; environment-scoped routes also use 404 without disclosing an object from the other environment.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "success": false,
              "data": null,
              "error": "the requested resource was not found",
              "code": "NOT_FOUND"
            }
          }
        }
      },
      "Conflict": {
        "description": "Idempotency-Key reused for a different request identity (route path or raw request body), a request already in progress, or a duplicate order_ref.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "success": false,
              "data": null,
              "error": "the Idempotency-Key was reused for a different request",
              "code": "IDEMPOTENCY_KEY_REUSED"
            }
          }
        }
      },
      "Unprocessable": {
        "description": "A structurally known request cannot run now — for example the exact live payin collection rail is not enabled, a destination is inactive/unverified for the operation, the same-currency available balance is insufficient (including an empty settlement sweep), or no matching P2P payout liquidity and no collection-rail fallback is available for a direct P2P payin (`P2P_LIQUIDITY_UNAVAILABLE`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "examples": {
              "insufficient_balance": {
                "value": {
                  "success": false,
                  "data": null,
                  "error": "insufficient available balance in this currency",
                  "code": "INSUFFICIENT_BALANCE"
                }
              },
              "p2p_liquidity_unavailable": {
                "value": {
                  "success": false,
                  "data": null,
                  "error": "no P2P liquidity is available for this amount and currency",
                  "code": "P2P_LIQUIDITY_UNAVAILABLE"
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Per-key rate limit exceeded — back off and retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "success": false,
              "data": null,
              "error": "too many requests — retry after the window resets",
              "code": "RATE_LIMITED"
            }
          }
        }
      }
    }
  }
}