{
  "openapi": "3.0.3",
  "info": {
    "title": "Payaries Payment API",
    "version": "1.0.0",
    "description": "Payaries (payaries.com; often searched or autocorrected as \"Prairie\", which is a different thing) is a multi-chain crypto payment gateway. Create an invoice, show the customer the one-time deposit address, and receive a signed webhook when the payment confirms. Withdraw to any address on demand.\n\n**Read this before calling the API.**\n- **Real money.** Payaries runs on mainnet only. There is no sandbox and no testnet: `bqp_test_` keys move real funds on the same chains as `bqp_live_` keys. Integration-test with a small real payment.\n- **Authentication.** `X-Api-Key` + `X-Timestamp` (unix ms) + `X-Signature` = hex HMAC-SHA256 of `timestamp + \"\\n\" + METHOD + \"\\n\" + request-target + \"\\n\" + JSON.stringify(body ?? {})`, keyed with the API secret. `request-target` is the path plus query string exactly as sent. A GET signs the body `{}`. Each signature on a POST/PATCH/DELETE works once, so every retry is re-signed.\n- **Idempotency.** Invoices: `referenceId`. Withdrawals: the `Idempotency-Key` header is required with an API key; reuse the same key on every retry.\n- **Onboarding first.** `POST /v1/invoices` returns `403 ONBOARDING_REQUIRED` until the merchant has submitted a business profile in the dashboard.\n- **Humans hold the keys.** Creating API keys, the webhook secret, the webhook URL, the IP whitelist, 2FA and withdrawal approval are dashboard-session-only. An API key can never do them, by design.\n- **Never fulfil an order on a wallet or explorer.** Fulfil on the invoice status (`confirmed` or later) from a verified webhook or `GET /v1/invoices/{id}`.\n\nNarrative guides, per-language signing examples and error handling: https://www.payaries.com/guides. Plain-text summary for language models: https://www.payaries.com/llms.txt.\n\nOperations carry `x-payaries-side-effects`: `read` (safe), `write` (changes account state, moves no money) or `funds` (can move or reserve real funds — require human approval).",
    "termsOfService": "https://www.payaries.com/terms",
    "contact": {
      "name": "Payaries",
      "url": "https://www.payaries.com/contact",
      "email": "connect@payaries.com"
    },
    "x-logo": {
      "url": "https://www.payaries.com/icon-512.png",
      "altText": "Payaries"
    }
  },
  "servers": [
    {
      "url": "https://bqpay-api-gateway.buildquick.tech",
      "description": "Payaries production gateway (real mainnet funds)"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    },
    {
      "SessionAuth": []
    }
  ],
  "tags": [
    {
      "name": "Webhooks",
      "description": "Where Payaries sends signed status events, and how to test and secure that endpoint."
    },
    {
      "name": "Chains & Rates",
      "description": "Public: the live chain/asset list and informational spot rates."
    },
    {
      "name": "Invoices",
      "description": "The deposit side: create an invoice, get a one-time deposit address, follow it to settlement."
    },
    {
      "name": "Withdrawals",
      "description": "The payout side: quote, create (idempotent), follow and cancel withdrawals."
    },
    {
      "name": "Account",
      "description": "Read-only balance, Payaries wallet, ledger and settlement data."
    },
    {
      "name": "Health",
      "description": "Liveness."
    }
  ],
  "paths": {
    "/v1/merchants/me/webhook": {
      "patch": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Set your webhook URL",
        "description": "Must be a public `https://` URL — `http://`, localhost, private/link-local IPs, single-label or internal hostnames and URLs with credentials are rejected (400), and the target is re-checked on every delivery (redirects are not followed). **Session-only** (an API key gets 403); needs a fresh single-use `totpCode` when 2FA is on.\n",
        "security": [
          {
            "SessionAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "webhookUrl"
                ],
                "properties": {
                  "webhookUrl": {
                    "$ref": "#/components/schemas/WebhookUrl"
                  },
                  "totpCode": {
                    "$ref": "#/components/schemas/TotpCode"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhookUrl": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "operationId": "setWebhookUrl",
        "x-payaries-side-effects": "write"
      }
    },
    "/v1/merchants/me/webhook-secret/rotate": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Create or rotate your webhook secret",
        "description": "Shown exactly once. Nothing requires a secret to already exist — your very first call here creates one, every call after that replaces it immediately (plan a cutover window). Session-only — an API key cannot rotate this out from under you.\n",
        "security": [
          {
            "SessionAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "New secret.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhookSecret": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Requires a dashboard session — not available via API key."
          }
        },
        "operationId": "rotateWebhookSecret",
        "x-payaries-side-effects": "write"
      }
    },
    "/v1/merchants/webhook-test": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Send a synthetic test webhook",
        "description": "Fires one `webhook.test` event to your configured URL, signed exactly like a real one, no retries.",
        "responses": {
          "200": {
            "description": "Sent."
          }
        },
        "operationId": "sendTestWebhook",
        "x-payaries-side-effects": "write"
      }
    },
    "/v1/chains": {
      "get": {
        "tags": [
          "Chains & Rates"
        ],
        "summary": "List active chain/asset pairs",
        "description": "Public — works without auth and then lists the platform's standard assets (a token only appears once its contract address is actually configured). If the call is authenticated (API key or dashboard session) it also lists **that merchant's own approved custom tokens** — in `assets` once usable, and in `customAssets` with their contract, decimals and activation time. Never another merchant's tokens.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "chains": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "chainId": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "assets": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "customAssets": {
                            "type": "array",
                            "description": "Only present for an authenticated merchant who has approved custom tokens on this chain.",
                            "items": {
                              "type": "object",
                              "properties": {
                                "chainId": {
                                  "type": "string"
                                },
                                "asset": {
                                  "type": "string",
                                  "example": "FOT_BEP20_c1a8"
                                },
                                "symbol": {
                                  "type": "string"
                                },
                                "name": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "contractAddress": {
                                  "type": "string"
                                },
                                "decimals": {
                                  "type": "integer"
                                },
                                "activatesAt": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true
                                },
                                "usable": {
                                  "type": "boolean"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "operationId": "listChains",
        "x-payaries-side-effects": "read"
      }
    },
    "/v1/rates/{asset}/{fiat}": {
      "get": {
        "tags": [
          "Chains & Rates"
        ],
        "summary": "Informational spot rate",
        "description": "NOT the rate an invoice locks — that happens at creation time. Use only for a pre-checkout estimate. Public, no auth.",
        "security": [],
        "parameters": [
          {
            "name": "asset",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "ETH"
          },
          {
            "name": "fiat",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "USD"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "asset": {
                      "type": "string"
                    },
                    "fiatCurrency": {
                      "type": "string"
                    },
                    "rate": {
                      "type": "string"
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "binance",
                        "coingecko"
                      ]
                    },
                    "fetchedAt": {
                      "type": "integer",
                      "description": "Unix ms"
                    }
                  }
                }
              }
            }
          }
        },
        "operationId": "getRate",
        "x-payaries-side-effects": "read"
      }
    },
    "/v1/invoices": {
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Create an invoice (the deposit side)",
        "description": "Rate-limited (20/min). `referenceId` is your idempotency key — replaying it returns the existing invoice.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateInvoiceRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed, an unsupported or disabled asset, or the invoice is under the minimum for a chain whose gas Payaries pays — `code` `BELOW_MINIMUM_INVOICE` with `chainId` and `minimumUsd` (a payment smaller than its own network fee can't be swept profitably; the value is admin-tunable).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "example": "BELOW_MINIMUM_INVOICE"
                    },
                    "chainId": {
                      "type": "string"
                    },
                    "minimumUsd": {
                      "type": "string",
                      "example": "30"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/FeeWalletInsufficient"
          },
          "403": {
            "description": "`code` `ACCOUNT_REJECTED`: the account application was rejected, so no transactions at all — test mode included. Otherwise, onboarding is mandatory before ANY invoice, test or live (API key or dashboard): `code` `ONBOARDING_REQUIRED` until the merchant has submitted their business profile (POST /v1/merchants/me/onboarding). Also returned, without that code, for a suspended/blocked account or when new deposits are disabled. Nothing is created.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "ONBOARDING_REQUIRED",
                        "ACCOUNT_REJECTED"
                      ],
                      "example": "ONBOARDING_REQUIRED"
                    },
                    "onboardingStatus": {
                      "type": "string",
                      "enum": [
                        "not_submitted",
                        "pending_verification",
                        "rejected",
                        "verified",
                        "unknown"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "operationId": "createInvoice",
        "x-payaries-side-effects": "write"
      },
      "get": {
        "tags": [
          "Invoices"
        ],
        "summary": "List your invoices",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/InvoiceStatus"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Invoice"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          }
        },
        "operationId": "listInvoices",
        "x-payaries-side-effects": "read"
      }
    },
    "/v1/invoices/{id}": {
      "get": {
        "tags": [
          "Invoices"
        ],
        "summary": "Get one invoice",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "404": {
            "description": "Not found."
          }
        },
        "operationId": "getInvoice",
        "x-payaries-side-effects": "read"
      }
    },
    "/v1/invoices/{id}/resend-webhook": {
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Re-fire the webhook for this invoice's current status",
        "description": "Use when `webhookStatus` is `\"failed\"`. Rate-limited (20/min).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Re-fired."
          }
        },
        "operationId": "resendInvoiceWebhook",
        "x-payaries-side-effects": "write"
      }
    },
    "/v1/withdrawals": {
      "post": {
        "tags": [
          "Withdrawals"
        ],
        "summary": "Withdraw to any address",
        "description": "Rate-limited (20/min). `requestedAmount` is debited from `available` only (never `pendingSweep`) and reserved; `netAmount` (after commission) is what actually broadcasts on-chain.\n**With an API key the `Idempotency-Key` header is REQUIRED.** The same key + same parameters returns the original withdrawal (200) instead of creating another; the same key with different parameters is 409. (Each request signature is also single-use, so a retry is re-signed but keeps the same Idempotency-Key.)\n**From a dashboard session** the account must have 2FA enabled (`403 two_factor_required` otherwise) and a fresh single-use `totpCode` (body or `X-Totp-Code` header; `401 totpRequired` without it).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/TotpHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "chainId",
                  "asset",
                  "destinationAddress",
                  "amount"
                ],
                "properties": {
                  "chainId": {
                    "type": "string",
                    "example": "tron-mainnet"
                  },
                  "asset": {
                    "type": "string",
                    "example": "USDT_TRC20"
                  },
                  "destinationAddress": {
                    "type": "string"
                  },
                  "amount": {
                    "type": "string",
                    "example": "50.00"
                  },
                  "totpCode": {
                    "$ref": "#/components/schemas/TotpCode"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An earlier request with the same Idempotency-Key — the ORIGINAL withdrawal is returned; nothing new was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Withdrawal"
                }
              }
            }
          },
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Withdrawal"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed, a missing/malformed Idempotency-Key (API key), the chain doesn't support withdrawal execution yet (currently btc-mainnet), or the amount can't cover its fees — `code` `BELOW_MINIMUM_WITHDRAWAL` or `AMOUNT_TOO_SMALL_FOR_NETWORK_FEE` (see `GET /v1/withdrawals/quote`)."
          },
          "401": {
            "$ref": "#/components/responses/AuthFailed"
          },
          "402": {
            "$ref": "#/components/responses/FeeWalletInsufficient"
          },
          "403": {
            "description": "Destination address is on the platform's blocked list, or (dashboard) 2FA is not enabled — `code: two_factor_required`."
          },
          "409": {
            "description": "Insufficient available balance, or this Idempotency-Key was already used with different parameters."
          },
          "503": {
            "description": "Fails closed (see FailedClosed), or `code` `NETWORK_FEE_UNAVAILABLE` — the network fee couldn't be priced right now; retry shortly."
          }
        },
        "operationId": "createWithdrawal",
        "x-payaries-side-effects": "funds"
      },
      "get": {
        "tags": [
          "Withdrawals"
        ],
        "summary": "List your withdrawals",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/WithdrawalStatus"
            }
          },
          {
            "name": "chainId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Withdrawal"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          }
        },
        "operationId": "listWithdrawals",
        "x-payaries-side-effects": "read"
      }
    },
    "/v1/withdrawals/quote": {
      "get": {
        "tags": [
          "Withdrawals"
        ],
        "summary": "Quote a withdrawal's fees",
        "description": "What a withdrawal of this size would cost, before you commit to it: the platform fee, the **network fee** (the gas Payaries pays for you, deducted inside the withdrawal — you never need to hold TRX, ETH or BNB) and the amount that would arrive. Runs the same pricing and checks as `POST /v1/withdrawals`, so a quote and the real charge differ only if a fee rule or the gas price moved in between. The network fee is fixed when the withdrawal is created and never changes afterwards.\n",
        "parameters": [
          {
            "name": "chainId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "example": "tron-mainnet"
            }
          },
          {
            "name": "asset",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "example": "USDT_TRC20"
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1000"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The quote.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "chainId": {
                      "type": "string"
                    },
                    "asset": {
                      "type": "string"
                    },
                    "requestedAmount": {
                      "type": "string"
                    },
                    "platformFee": {
                      "type": "string"
                    },
                    "platformFeePaidFrom": {
                      "type": "string",
                      "enum": [
                        "transaction",
                        "fee_wallet"
                      ],
                      "description": "Best case — an optional fee wallet that turns out too short falls back to `transaction` at creation."
                    },
                    "networkFee": {
                      "type": "string",
                      "description": "`\"0\"` when not charged."
                    },
                    "networkFeeIncluded": {
                      "type": "boolean",
                      "description": "True when Payaries covers the gas and charges nothing for it — show \"Network fee: Included\"."
                    },
                    "gasSponsored": {
                      "type": "boolean",
                      "description": "Payaries pays the gas for this transaction. False = the chain's gas station is off and your own wallet pays gas, as before."
                    },
                    "netAmount": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed query, or the amount can't be withdrawn: `code` `BELOW_MINIMUM_WITHDRAWAL` (under the platform's minimum, `minimumUsd` given) or `AMOUNT_TOO_SMALL_FOR_NETWORK_FEE` (the network fee would take more than half of what is left after the platform fee; `networkFee` and `minimumAmount` given).\n"
          },
          "401": {
            "$ref": "#/components/responses/AuthFailed"
          },
          "503": {
            "description": "`code` `NETWORK_FEE_UNAVAILABLE` — the fee couldn't be priced right now; retry shortly. Nothing is ever charged on a guess."
          }
        },
        "operationId": "quoteWithdrawal",
        "x-payaries-side-effects": "read"
      }
    },
    "/v1/withdrawals/{id}": {
      "get": {
        "tags": [
          "Withdrawals"
        ],
        "summary": "Get one withdrawal",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Withdrawal"
                }
              }
            }
          },
          "404": {
            "description": "Not found."
          }
        },
        "operationId": "getWithdrawal",
        "x-payaries-side-effects": "read"
      }
    },
    "/v1/withdrawals/{id}/cancel": {
      "post": {
        "tags": [
          "Withdrawals"
        ],
        "summary": "Cancel a pending withdrawal",
        "description": "Only while `status` is `pending_approval` — releases the reservation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Withdrawal"
                }
              }
            }
          },
          "409": {
            "description": "Can't cancel a withdrawal that's already past pending_approval."
          }
        },
        "operationId": "cancelWithdrawal",
        "x-payaries-side-effects": "write"
      }
    },
    "/v1/merchants/me/balance": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Your wallet balance, per chain/asset",
        "description": "Three buckets per {chainId, asset}: `available` (withdrawable now), `pendingSweep` (credited, not yet swept to your own Payaries wallet — not withdrawable yet), `pending` (reserved by an in-flight withdrawal).\n",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "balances": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "chainId": {
                            "type": "string"
                          },
                          "asset": {
                            "type": "string"
                          },
                          "available": {
                            "type": "string"
                          },
                          "pending": {
                            "type": "string"
                          },
                          "pendingSweep": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "operationId": "getBalance",
        "x-payaries-side-effects": "read"
      }
    },
    "/v1/merchants/me/custodial-wallets": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Your dedicated on-chain Payaries wallets",
        "description": "One address per chain-namespace (shared across EVM chains — the same underlying key — separate for BTC/Tron). This is where your confirmed deposits actually land (not a shared pool with other merchants) and what withdrawals sign from — verify it, and its live balances, on a block explorer any time. Where `gasSponsored` is false, withdrawal gas is never auto-topped-up — check `balances` for the native asset before withdrawing a token. Where it is true, you need none.\n",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "wallets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "chainId": {
                            "type": "string"
                          },
                          "address": {
                            "type": "string"
                          },
                          "gasSponsored": {
                            "type": "boolean",
                            "description": "True when Payaries currently pays this chain's gas (admin-enabled, not paused), so you need NO native currency here — gas is recovered as a network fee inside the transaction. False: your own wallet pays gas, keep a little native currency in it. Re-read it rather than caching: an admin can switch a chain on, off or pause it."
                          },
                          "balances": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "asset": {
                                  "type": "string"
                                },
                                "amount": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "null means this specific on-chain balance check failed (usually a flaky RPC endpoint) — not that the balance is zero."
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "wallets": [
                    {
                      "chainId": "eth-mainnet",
                      "address": "0x...",
                      "balances": [
                        {
                          "asset": "ETH",
                          "amount": "0.05"
                        },
                        {
                          "asset": "USDT_ERC20",
                          "amount": "100.0"
                        }
                      ]
                    },
                    {
                      "chainId": "bsc-mainnet",
                      "address": "0x...",
                      "balances": [
                        {
                          "asset": "BNB",
                          "amount": "0.02"
                        }
                      ]
                    },
                    {
                      "chainId": "tron-mainnet",
                      "address": "T...",
                      "balances": [
                        {
                          "asset": "TRX",
                          "amount": "5.0"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          }
        },
        "operationId": "listCustodialWallets",
        "x-payaries-side-effects": "read"
      }
    },
    "/v1/merchants/me/ledger": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Raw double-entry rows behind your balance",
        "parameters": [
          {
            "name": "asset",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chainId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          }
        },
        "operationId": "listLedger",
        "x-payaries-side-effects": "read"
      }
    },
    "/v1/settlements": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "The sweep record for each invoice",
        "description": "One per invoice — no batched multi-invoice settlement. This is NOT where withdrawals show up.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chainId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          }
        },
        "operationId": "listSettlements",
        "x-payaries-side-effects": "read"
      }
    },
    "/health": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Liveness check",
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    },
                    "service": {
                      "type": "string",
                      "example": "api-gateway"
                    },
                    "security": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "degraded"
                      ],
                      "description": "State of the Redis-backed security controls (replay guard, single-use codes, lockouts, session revocation). `degraded` means they are failing closed/open per configuration."
                    }
                  }
                }
              }
            }
          }
        },
        "operationId": "getHealth",
        "x-payaries-side-effects": "read"
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Server-to-server auth. Also requires `X-Signature` (hex HMAC-SHA256(timestamp + \"\\n\" + METHOD + \"\\n\" + request-target + \"\\n\" + JSON.stringify(body ?? {}), apiSecret) — request-target is the path plus query string as sent; each signature is single-use for state-changing requests) and `X-Timestamp` (unix ms, ±5 min skew) headers — OpenAPI's apiKey type can't express the signature; see docs/USER_INTEGRATION_GUIDE.md §3.1 for the exact signing recipe, or use the Node SDK (which signs for you). The old `timestamp + body` scheme is rejected. A request that fails signature checks, replays a used signature, or comes from an IP with many recent failures is refused (401 / 429).\n"
      },
      "SessionAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "From POST /v1/auth/login. Most routes accept this OR ApiKeyAuth, except POST /v1/invoices always records keyMode \"test\" when created via a session, and the account-protection routes marked session-only. Sessions can be revoked server-side (password change, log out everywhere), so a 401 at any time means log in again. Sensitive actions additionally need a fresh single-use authenticator code (`totpCode` or `X-Totp-Code`).\n"
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "REQUIRED when creating a withdrawal with an API key (8-128 chars of `A-Za-z0-9_-:.`). Same key + same parameters returns the original.",
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 128,
          "example": "wd_2026_09_30_0001"
        }
      },
      "TotpHeader": {
        "name": "X-Totp-Code",
        "in": "header",
        "required": false,
        "description": "A fresh, single-use authenticator code, as an alternative to `totpCode` in the JSON body.",
        "schema": {
          "type": "string",
          "example": "123456"
        }
      }
    },
    "responses": {
      "FeeWalletInsufficient": {
        "description": "The asset requires a fee wallet balance (see `requiredForAssets` on GET /v1/merchants/me/fee-wallet) and yours can't cover this transaction's fee. Top up via POST /v1/merchants/me/fee-wallet/topups and retry. Nothing was created or reserved.\n",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "code": {
                  "type": "string",
                  "example": "FEE_WALLET_INSUFFICIENT"
                },
                "requiredUsd": {
                  "type": "string",
                  "description": "Estimated fee in USD."
                }
              }
            }
          }
        }
      },
      "AuthFailed": {
        "description": "Missing/invalid credentials, expired or revoked session, bad or replayed request signature (`Request signature has already been used`), stale timestamp, or a step-up problem (`totpRequired: true` = send a fresh code; a previously used code is refused).\n",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "totpRequired": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "TotpCode": {
        "type": "string",
        "pattern": "^\\d{6}$",
        "description": "A fresh, single-use 6-digit authenticator code. Required for sensitive actions when 2FA is enabled.",
        "example": "123456"
      },
      "WebhookUrl": {
        "type": "string",
        "format": "uri",
        "description": "A public `https://` URL. Refused: `http://`, localhost, private/link-local IPs, single-label or `.internal`/`.local` hostnames, embedded credentials. Re-validated on every delivery; redirects are not followed.\n",
        "example": "https://shop.example.com/webhooks/bqpay"
      },
      "InvoiceStatus": {
        "type": "string",
        "enum": [
          "pending",
          "detected",
          "confirming",
          "confirmed",
          "forwarding",
          "settled",
          "underpaid",
          "expired",
          "failed"
        ]
      },
      "WithdrawalStatus": {
        "type": "string",
        "enum": [
          "pending_approval",
          "processing",
          "broadcast",
          "confirmed",
          "rejected",
          "cancelled",
          "failed"
        ]
      },
      "Pagination": {
        "type": "object",
        "properties": {
          "page": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "pages": {
            "type": "integer"
          }
        }
      },
      "CreateInvoiceRequest": {
        "type": "object",
        "required": [
          "referenceId",
          "crypto"
        ],
        "description": "Send exactly one of `fiat` (standard assets, priced by the rate engine) or `crypto.amount` (a custom token, in token units — it has no price feed).\n",
        "properties": {
          "referenceId": {
            "type": "string",
            "maxLength": 128,
            "description": "Your own order ID — doubles as the idempotency key."
          },
          "fiat": {
            "type": "object",
            "required": [
              "amount",
              "currency"
            ],
            "properties": {
              "amount": {
                "type": "string",
                "example": "49.99"
              },
              "currency": {
                "type": "string",
                "minLength": 3,
                "maxLength": 3,
                "example": "USD"
              }
            }
          },
          "crypto": {
            "type": "object",
            "required": [
              "asset",
              "chainId"
            ],
            "properties": {
              "asset": {
                "type": "string",
                "example": "USDT_TRC20"
              },
              "chainId": {
                "type": "string",
                "example": "tron-mainnet"
              },
              "amount": {
                "type": "string",
                "example": "1250.5",
                "description": "Custom tokens only: the amount in token units (no more decimals than the token has). Send this instead of `fiat`."
              }
            }
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "successUrl": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "https URL the checkout page sends the customer to once paid. Its host must be in your redirect domains (`/v1/merchants/me/redirect-settings`), else 400 `REDIRECT_URL_NOT_ALLOWED`. Readable by anyone with the checkout link — put no secret in it, and never treat reaching it as proof of payment."
          },
          "cancelUrl": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "https URL the customer can return to if they back out. Same rules as `successUrl`."
          }
        }
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "_id": {
            "type": "string"
          },
          "merchantId": {
            "type": "string"
          },
          "keyMode": {
            "type": "string",
            "enum": [
              "test",
              "live"
            ]
          },
          "referenceId": {
            "type": "string"
          },
          "checkoutId": {
            "type": "string",
            "description": "Random, unguessable id for the customer checkout link (…/pay/<checkoutId>). Share this with your customer, not `_id`."
          },
          "successUrl": {
            "type": "string"
          },
          "cancelUrl": {
            "type": "string"
          },
          "fiat": {
            "type": "object",
            "properties": {
              "amount": {
                "type": "string"
              },
              "currency": {
                "type": "string"
              }
            }
          },
          "crypto": {
            "type": "object",
            "properties": {
              "asset": {
                "type": "string"
              },
              "chainId": {
                "type": "string"
              },
              "amountDue": {
                "type": "string"
              },
              "amountReceived": {
                "type": "string"
              },
              "rateLocked": {
                "type": "string"
              },
              "rateLockedAt": {
                "type": "string",
                "format": "date-time"
              },
              "contractAddress": {
                "type": "string",
                "description": "Token invoices only: the contract the platform watches for this asset, exactly as configured. Absent for a native coin."
              },
              "decimals": {
                "type": "integer",
                "description": "Token invoices only: the token's decimals on this chain (USDT is 6 on Ethereum/Tron, 18 on BSC). Absent for a native coin."
              }
            }
          },
          "depositAddress": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string"
              },
              "derivationPath": {
                "type": "string"
              },
              "index": {
                "type": "integer"
              }
            }
          },
          "status": {
            "$ref": "#/components/schemas/InvoiceStatus"
          },
          "confirmations": {
            "type": "object",
            "properties": {
              "required": {
                "type": "integer"
              },
              "current": {
                "type": "integer"
              }
            }
          },
          "txHashes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "webhookStatus": {
            "type": "string",
            "enum": [
              "pending",
              "delivered",
              "failed"
            ]
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "latePaymentDetectedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Present only if a payment was MINED after the invoice expired. It is held for review by the Payaries team: the invoice stays `expired`, nothing is credited and no status webhook is sent. If the team settles it, the invoice moves `expired → detected|underpaid` and that webhook carries `data.latePayment: true`; if they refund it, nothing further happens. (A payment made before the deadline but noticed after it is not late: it moves the invoice out of `expired` normally, without the flag.)\n"
          },
          "unclaimedDeposits": {
            "type": "array",
            "description": "Present only when money reached the deposit address but is NOT part of what was credited (a second payment after settlement, or a deposit that hadn't confirmed at credit time). Safe at the address, never credited automatically — contact support to refund or credit.\n",
            "items": {
              "type": "object",
              "properties": {
                "txHash": {
                  "type": "string"
                },
                "amount": {
                  "type": "string"
                },
                "asset": {
                  "type": "string"
                },
                "detectedAt": {
                  "type": "string",
                  "format": "date-time"
                },
                "reason": {
                  "type": "string"
                }
              }
            }
          },
          "payout": {
            "type": "object",
            "nullable": true,
            "description": "Your own Payaries wallet sweep for this invoice — informational only, see docs/USER_INTEGRATION_GUIDE.md §5.3.",
            "properties": {
              "toAddress": {
                "type": "string"
              },
              "amount": {
                "type": "string"
              },
              "status": {
                "type": "string"
              },
              "txHash": {
                "type": "string"
              }
            }
          },
          "feePaidFrom": {
            "type": "string",
            "enum": [
              "transaction",
              "fee_wallet"
            ],
            "description": "Set once confirmed. `fee_wallet` = the fee was charged to your fee wallet and the full amount credited/swept (commission is zero); `transaction` = deducted from the amount. Absent on invoices confirmed before the fee wallet existed (treat as `transaction`).\n"
          },
          "feeWalletChargedUsd": {
            "type": "string",
            "description": "Only when feePaidFrom is fee_wallet — the USD debited."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Withdrawal": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "chainId": {
            "type": "string"
          },
          "asset": {
            "type": "string"
          },
          "destinationAddress": {
            "type": "string"
          },
          "requestedAmount": {
            "type": "string"
          },
          "feeAmount": {
            "type": "string"
          },
          "networkFeeAmount": {
            "type": "string",
            "description": "The network fee deducted inside this withdrawal — the gas Payaries pays on your behalf, so you never hold TRX/ETH/BNB. `\"0\"` when none was charged (gas under the platform's threshold — shown as \"Included\" — or the chain's gas station is off). `netAmount = requestedAmount - feeAmount - networkFeeAmount`."
          },
          "netAmount": {
            "type": "string",
            "description": "What arrives at the destination: requestedAmount minus feeAmount minus networkFeeAmount. Equals requestedAmount minus networkFeeAmount (and feeAmount is zero) when the fee is paid from the fee wallet."
          },
          "feePaidFrom": {
            "type": "string",
            "enum": [
              "transaction",
              "fee_wallet"
            ],
            "description": "Absent on withdrawals created before the fee wallet existed (treat as `transaction`)."
          },
          "feeWalletReservedUsd": {
            "type": "string",
            "description": "Only when feePaidFrom is fee_wallet — USD held from the wallet until the withdrawal confirms (charged) or fails/is rejected/cancelled (returned)."
          },
          "status": {
            "$ref": "#/components/schemas/WithdrawalStatus"
          },
          "autoApproved": {
            "type": "boolean"
          },
          "txHash": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "Guides, signing examples and the plain-text summary for AI assistants",
    "url": "https://www.payaries.com/guides"
  },
  "x-llms-txt": "https://www.payaries.com/llms.txt"
}
