openapi: 3.1.0
info:
  title: TgPay Crypto Merchant API
  version: '2026-11-03'
  summary: Merchant payments API of the TgPay Crypto wallet (@tgpaycryptobot).
  description: |
    The Merchant API of TgPay Crypto (@tgpaycryptobot): create
    invoices, refund them, send payouts and checks, manage recurring
    subscriptions, and receive signed webhooks.

    Human-readable documentation: https://docs.tgpaycrypto.com/crypto-pay-api-reference/

    ## Conventions

    - **Authentication** — send your token in `TgPayCrypto-API-Token`.
      `TgCryptoPay-API-Token` and `Crypto-Pay-API-Token` are accepted aliases,
      in that order. The first non-empty header wins. Full-access primary
      tokens have all scopes; restricted tokens need the operation's scope.
    - **Envelope** — application success is HTTP 200 with `{ok: true, result}`;
      application errors are `{ok: false, error: {code, name}}`, with `code`
      equal to the HTTP status. Branch on `error.name`. Transport/framework
      rejections, such as malformed typed query parameters, may lack this envelope.
    - **Amounts** — requests use decimal strings in whole-coin or fiat units,
      never JSON numbers. Crypto input is rounded to the asset's minor unit,
      with midpoint rounding away from zero; the result must be positive.
      Fiat input must represent a positive exact number of cents, at most
      10^13 whole fiat units. Responses use fixed-point strings; balance and
      fee amounts can be zero. `*_minor` values are integer strings.
      `getCurrencies` supplies asset decimals. Send exact asset precision.
    - **POST parameters** — JSON object, form-urlencoded body, or URL query
      parameters; body values override query values, including null. Repeated
      form/query keys use the last value. Multipart is rejected. Money-moving
      methods have no GET aliases. Nested `items` and `recipients` must be
      JSON arrays in a JSON body; JSON-encoded strings do not work for them.
      Required fields must be present after merging query and body.
      Optional null values generally behave as omitted; per-field rules apply.
      Integers accept integer strings; booleans also accept 1/0 and the strings
      true/false, 1/0, yes/no, on/off (case-insensitive, surrounding whitespace ignored).
      Text limits are measured in UTF-16 code units by the server.
    - **Idempotency** — `spend_id` is required for transfers and batch items,
      optional for checks and refunds. Empty/null optional keys disable replay.
      Keys are scoped per app and operation; transfer and transferBatch share
      a namespace. After validation and access gates, the same parsed request
      replays its stored success. Different input returns `409 idempotency_conflict`;
      an unfinished claim returns `409 idempotency_in_progress`. Amount strings
      and recipient spelling are not normalized for the request hash: keep them
      identical on retries. Replays are snapshots, not refreshed objects; use
      list methods to obtain current status. createInvoice and plan creation
      have no spend_id idempotency.
    - **Pagination** — invoices, checks and transfers default to offset 0,
      count 100; the server clamps offset to at least 0 and count to 1..1000.
      Subscriptions clamp count to 1..500. These lists return `{items: [...]}`,
      newest first. getSubscriptionPlans returns all plans without pagination.
    - **Timestamps** — ISO-8601 strings; nullable timestamps are explicitly null.
    - **Rate limits** — per app across its tokens: createInvoice/createCheck
      60/min, refundInvoice/transfer 30/min, transferBatch/updateApp 10/min,
      createSubscriptionPlan 20/min. Additional request limits may apply.
      On `429 rate_limited`, increase the delay before retrying.
    - **Maintenance** — POST methods return `503 maintenance`; GET methods
      are not blocked by maintenance.

    ## Shared application errors

    | HTTP | error.name | When |
    |---|---|---|
    | 401 | unauthorized | Missing/invalid/revoked/expired token, inactive app or blocked owner |
    | 403 | scope_required | Token lacks the operation's scope |
    | 400 | invalid_request | Invalid body, field type, length or numeric bounds |
    | 429 | rate_limited | Request limit reached; increase the delay before retrying |
    | 503 | maintenance | Maintenance blocks POST methods |
    | 500 | internal_error | Unexpected server error |

    transfer, transferBatch, createCheck and refundInvoice can also return
    `503 merchant_payouts_paused` or `403 verification_required` when outgoing
    app funds are restricted. transfer, transferBatch and createCheck enforce
    the owner's payout mode (`403 payouts_disabled` / `recipient_not_allowed`)
    and rolling daily payout ceiling (`403 daily_cap_exceeded`). Refunds do
    not use those payout-mode/ceiling gates. getMe reports the mode and ceiling;
    the owner changes them in the app, not through this API.
externalDocs:
  description: Merchant API reference (developer docs)
  url: https://docs.tgpaycrypto.com/crypto-pay-api-reference/
servers:
  - url: https://app.tgpaycrypto.com/pay/api
    description: Production
  - url: https://testnet.tgpaycrypto.com/pay/api
    description: Testnet — coins with no value, its own bot @tgpaycrypto_testnet_bot and its own apps and tokens
security:
  - apiToken: []
  - apiTokenLegacy: []
  - apiTokenCompat: []
tags:
  - name: app
    description: App identity, balance, currencies, rates, stats.
  - name: invoices
    description: Payment requests and refunds.
  - name: checks
    description: Claimable crypto checks funded from the app balance, including named recipient lists.
  - name: transfers
    description: Payouts to Telegram users from the app balance.
  - name: subscriptions
    description: 'Extension: recurring billing — plans and subscriptions.'
paths:
  /getMe:
    get:
      tags: [app]
      operationId: getMe
      summary: App and token info
      description: |
        Scope: none — any valid token.

        Returns app identity, webhook settings, this token's scopes and label, and
        read-only payout controls. scopes and token_name are null for the primary token.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    $ref: '#/components/schemas/AppInfo'
        default:
          $ref: '#/components/responses/Error'
  /getBalance:
    get:
      tags: [app]
      operationId: getBalance
      summary: App balances per asset
      description: |
        Scope: `read`.

        One row per enabled asset, including zero balances. available is spendable
        app balance. onhold includes unclaimed check funds and pending username
        transfers. deleteCheck refunds a check's unclaimed remainder; pending
        transfers return to the app when processed for expiry. amount_minor is
        available in minor units, not available plus onhold.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    type: array
                    items:
                      $ref: '#/components/schemas/BalanceRow'
        default:
          $ref: '#/components/responses/Error'
  /getCurrencies:
    get:
      tags: [app]
      operationId: getCurrencies
      summary: Supported currencies
      description: |
        Scope: none — any valid token.

        The authoritative list of crypto assets (`is_blockchain: true`) and
        invoice-pricing fiats (`is_fiat: true`), with per-currency
        `decimals`. Treat this as the source of truth for what's live and
        for amount math.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    type: array
                    items:
                      $ref: '#/components/schemas/Currency'
        default:
          $ref: '#/components/responses/Error'
  /getExchangeRates:
    get:
      tags: [app]
      operationId: getExchangeRates
      summary: Crypto→fiat exchange rates
      description: |
        Scope: none — any valid token.

        Returns indicative crypto-to-fiat quotes. is_valid=true does not guarantee
        that a quote is current. If quotes are unavailable, the result may be an
        empty array. Use the invoice's recorded amounts and rates to reconcile
        an actual payment.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    type: array
                    items:
                      $ref: '#/components/schemas/ExchangeRate'
        default:
          $ref: '#/components/responses/Error'
  /getStats:
    get:
      tags: [app]
      operationId: getStats
      summary: App volume/conversion stats
      description: |
        Scope: `read`.

        Defaults to the last 24 hours. Naive datetimes are read as UTC; a
        `Z` suffix is accepted.

        Specific errors: `400 invalid_date` (unparsable `start_at`/`end_at`).
      parameters:
        - name: start_at
          in: query
          schema:
            type: string
            format: date-time
          description: Window start (default now − 24 h).
        - name: end_at
          in: query
          schema:
            type: string
            format: date-time
          description: Window end (default now).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    $ref: '#/components/schemas/AppStats'
        default:
          $ref: '#/components/responses/Error'
  /createInvoice:
    post:
      tags: [invoices]
      operationId: createInvoice
      summary: Create an invoice
      description: |
        Scope: `invoices`. Rate limit: 60/min per app.

        Crypto mode (default) fixes `asset`; fiat mode fixes `fiat` and the
        payer picks one of `accepted_assets` at pay time. Invoices are paid
        from the payer's in-app wallet balance; a payer may also fund the
        invoice from an external wallet (their deposit lands in their own
        wallet). Automatic payment requires sufficient available balance, an
        unexpired payment request and invoice, and no outgoing-payment restriction.
        Confirm the invoice is `paid` before fulfilling the order. The platform fee comes out of the
        merchant's leg and is reported in `fee_amount`.

        Fiat invoices convert at the fresh pay-time rate rounded up in the
        merchant's favor — unless a live `rate_lock_seconds` stamp applies.
        If no fresh rate is available, payment fails payer-side; the
        merchant is never settled at a stale rate.

        Specific errors:

        | HTTP | `error.name` | when |
        |---|---|---|
        | 400 | `invalid_currency` | missing asset in crypto mode; fiat mode without fiat or with non-empty asset; open_amount on a fiat invoice |
        | 400 | `invalid_amount` | missing/unparsable/non-positive amount; `amount`+`open_amount` together; fiat amount not an exact number of cents or > 10^13 |
        | 400 | `unknown_asset` | unknown/disabled `asset`, `swap_to`, or `accepted_assets` entry |
        | 400 | `unsupported_fiat` | `fiat` not supported |
        | 400 | `paid_btn_url_required` | `paid_btn_name` set without a valid http(s) `paid_btn_url` |
        | 400 | `rate_lock_fiat_only` | positive `rate_lock_seconds` on a crypto-priced invoice |
        | 409 | `ratelock_disabled` | rate-lock currently unavailable platform-side |
        | 409 | `rate_unavailable` | no fresh rate for an accepted asset — retry |

        Crypto mode ignores fiat and accepted_assets. A positive rate lock is
        fiat-only; zero disables it. A lapsed lock reverts to fresh pay-time rates.
        Creation has no spend_id replay protection. Optional payer-facing text
        may be withheld on the payment screen while under review.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateInvoiceRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/CreateInvoiceRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    $ref: '#/components/schemas/Invoice'
        default:
          $ref: '#/components/responses/Error'
      parameters:
        - name: currency_type
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/currency_type'
        - name: asset
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/asset'
        - name: fiat
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/fiat'
        - name: accepted_assets
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            type: string
            description: Comma-separated values.
        - name: amount
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/amount'
        - name: open_amount
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/open_amount'
        - name: description
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/description'
        - name: hidden_message
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/hidden_message'
        - name: payload
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/payload'
        - name: allow_comments
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/allow_comments'
        - name: allow_anonymous
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/allow_anonymous'
        - name: paid_btn_name
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/paid_btn_name'
        - name: paid_btn_url
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/paid_btn_url'
        - name: swap_to
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/swap_to'
        - name: expires_in
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/expires_in'
        - name: rate_lock_seconds
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateInvoiceRequest/properties/rate_lock_seconds'
  /getInvoices:
    get:
      tags: [invoices]
      operationId: getInvoices
      summary: List invoices
      description: |
        Scope: `read`. Newest first.

        status=active excludes invoices past their deadline; status=expired
        includes both stored expired rows and active rows past the deadline.
        The asset filter matches the stored crypto asset, including the settlement
        asset of a paid fiat invoice. Deleted invoices are absent. Non-numeric
        entries in invoice_ids are ignored; no valid ids means an empty list.
      parameters:
        - name: asset
          in: query
          schema:
            type: string
          description: Filter by crypto asset code.
        - name: fiat
          in: query
          schema:
            type: string
          description: Filter by fiat code.
        - name: invoice_ids
          in: query
          schema:
            type: string
          description: Comma-separated invoice ids.
        - name: status
          in: query
          schema:
            type: string
            enum: [active, paid, expired]
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
        - name: count
          in: query
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 1000
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    type: object
                    required: [items]
                    properties:
                      items:
                        type: array
                        items:
                          $ref: '#/components/schemas/Invoice'
        default:
          $ref: '#/components/responses/Error'
  /deleteInvoice:
    post:
      tags: [invoices]
      operationId: deleteInvoice
      summary: Delete an unpaid invoice
      description: |
        Scope: `invoices`.

        Cancels an **unpaid** invoice (active or expired). Result: `true`.

        Specific errors: `404 invoice_not_found` (unknown id or another
        app's invoice); `409 invoice_already_paid` (paid invoices can't be
        deleted — money moved).
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              required: [invoice_id]
              properties:
                invoice_id:
                  oneOf:
                    - type: integer
                      maximum: 9223372036854775807
                      minimum: 1
                    - type: string
                      pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
                  description: Integer or integer string; parsed value must be within 1..9223372036854775807.
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [invoice_id]
              properties:
                invoice_id:
                  oneOf:
                    - type: integer
                      maximum: 9223372036854775807
                      minimum: 1
                    - type: string
                      pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
                  description: Integer or integer string; parsed value must be within 1..9223372036854775807.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    const: true
        default:
          $ref: '#/components/responses/Error'
      parameters:
        - name: invoice_id
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            oneOf:
              - type: integer
                maximum: 9223372036854775807
                minimum: 1
              - type: string
                pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
            description: Integer or integer string; parsed value must be within 1..9223372036854775807.
  /refundInvoice:
    post:
      tags: [invoices]
      operationId: refundInvoice
      summary: Refund a paid invoice
      description: |
        Scope: `refunds`. Rate limit: 30/min
        per app.

        Returns a paid invoice's face amount — or part of it — from the app
        balance to the payer, anonymous payers included, without revealing
        them. The platform fee is not returned. Invoice `status` stays
        `paid`; progress is tracked in the cumulative `refunded_amount` /
        `refunded_minor`, and `refunded_at` is stamped once fully refunded.
        Triggers the opt-in `refund_completed` webhook.

        Specific errors:

        | HTTP | `error.name` | when |
        |---|---|---|
        | 404 | `invoice_not_found` | unknown id / another app's invoice |
        | 400 | `invalid_amount` | unparsable or non-positive `amount` |
        | 409 | `invoice_not_paid` | invoice isn't `paid` |
        | 409 | `already_refunded` | nothing left to refund |
        | 409 | `amount_too_big` | `amount` exceeds the unrefunded remainder |
        | 409 | `insufficient_funds` | app balance can't cover the refund |
        | 409 | `idempotency_conflict` / `idempotency_in_progress` | see Conventions → Idempotency |
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefundInvoiceRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/RefundInvoiceRequest'
      responses:
        '200':
          description: OK — the updated invoice with cumulative `refunded_*` fields.
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    $ref: '#/components/schemas/Invoice'
        default:
          $ref: '#/components/responses/Error'
      parameters:
        - name: invoice_id
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/RefundInvoiceRequest/properties/invoice_id'
        - name: amount
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/RefundInvoiceRequest/properties/amount'
        - name: spend_id
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/RefundInvoiceRequest/properties/spend_id'
  /createCheck:
    post:
      tags: [checks]
      operationId: createCheck
      summary: Create a check
      description: |
        Scope: `checks`. Rate limit: 60/min per app. Shared payout restrictions apply.

        Without recipients, creates a single-use check for amount. With recipients,
        creates one claim per named recipient and funds the sum of their amounts;
        top-level amount may be omitted, or is validated then ignored. Named lists
        cannot be combined with a pin. Amounts are locked immediately and included
        in getBalance.onhold. The unclaimed remainder returns on deletion or expiry.
        The app owner cannot claim their own check. Password and expiry are optional.

        Known recipient usernames bind to their user ids at creation; unknown
        usernames remain claimable by the matching user when they join. Numeric
        recipient ids need not already exist. A pin_to_username must resolve to an
        existing available user; pin_to_user_id takes precedence over it.

        check_activated fires on a single-use claim, or once per named seat with
        activated_slot. check_expired currently fires only for expired named lists.
        Optional non-empty spend_id enables stored-response replay.

        Specific errors: `400 unknown_asset`, `400 invalid_amount`,
        `400 invalid_request` (including non-positive pin_to_user_id),
        `400 invalid_recipient` (malformed named recipient or pin username),
        `400 invalid_pinned_user` (named list combined with pin),
        `400 invalid_slots` (more than 100 recipients), `400 self_claim`,
        `400 duplicate_recipient`, `404 user_not_found` (unresolved/unavailable pin),
        `400 amount_too_small` / `amount_too_big` (total outside payout USD bounds),
        `409 insufficient_funds`, `409 idempotency_conflict` / `idempotency_in_progress`.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCheckRequest'
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [asset, amount]
              properties:
                asset:
                  type: string
                  maxLength: 16
                amount:
                  $ref: '#/components/schemas/AmountInput'
                pin_to_user_id:
                  anyOf:
                    - oneOf:
                        - type: integer
                          maximum: 9223372036854775807
                          minimum: 1
                        - type: string
                          pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
                      description: Restrict claiming to this Telegram user id.
                    - type: 'null'
                pin_to_username:
                  anyOf:
                    - type: string
                      maxLength: 64
                      description: Restrict by @username (leading @ optional, case-insensitive); ignored
                        when pin_to_user_id is also given. Must belong to an existing user of the platform.
                    - type: 'null'
                spend_id:
                  anyOf:
                    - type: string
                      maxLength: 64
                      description: Optional replay key; empty/null disables idempotency.
                    - type: 'null'
                password:
                  anyOf:
                    - type: string
                      maxLength: 64
                      description: Optional claim password; empty/null means no password. Never returned.
                    - type: 'null'
                expires_in:
                  anyOf:
                    - oneOf:
                        - type: integer
                          maximum: 9223372036854775807
                          minimum: 0
                        - type: string
                          pattern: ^\s*(?:\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)|[+-]?0+)\s*$
                      description: Seconds until expiry; zero, null or omitted means no explicit deadline.
                        Choose a duration that produces a representable expiry timestamp.
                    - type: 'null'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    $ref: '#/components/schemas/Check'
        default:
          $ref: '#/components/responses/Error'
      parameters:
        - name: asset
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateCheckRequest/properties/asset'
        - name: amount
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateCheckRequest/properties/amount'
        - name: pin_to_user_id
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateCheckRequest/properties/pin_to_user_id'
        - name: pin_to_username
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateCheckRequest/properties/pin_to_username'
        - name: spend_id
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateCheckRequest/properties/spend_id'
        - name: password
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateCheckRequest/properties/password'
        - name: expires_in
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreateCheckRequest/properties/expires_in'
  /deleteCheck:
    post:
      tags: [checks]
      operationId: deleteCheck
      summary: Cancel an active check
      description: |
        Scope: `checks`.

        Cancels a stored-active check and refunds only its unclaimed remainder.
        Result: true. Specific errors: `404 check_not_found` (unknown or another
        app's check), `409 check_not_active` (already drained, deleted or expired).
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              required: [check_id]
              properties:
                check_id:
                  oneOf:
                    - type: integer
                      maximum: 9223372036854775807
                      minimum: 1
                    - type: string
                      pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
                  description: Integer or integer string; parsed value must be within 1..9223372036854775807.
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [check_id]
              properties:
                check_id:
                  oneOf:
                    - type: integer
                      maximum: 9223372036854775807
                      minimum: 1
                    - type: string
                      pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
                  description: Integer or integer string; parsed value must be within 1..9223372036854775807.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    const: true
        default:
          $ref: '#/components/responses/Error'
      parameters:
        - name: check_id
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            oneOf:
              - type: integer
                maximum: 9223372036854775807
                minimum: 1
              - type: string
                pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
            description: Integer or integer string; parsed value must be within 1..9223372036854775807.
  /getChecks:
    get:
      tags: [checks]
      operationId: getChecks
      summary: List checks
      description: |
        Scope: `read`. Newest first. Deleted checks are excluded. status=active
        selects checks that can still be claimed, status=activated fully claimed
        checks, status=expired (extension) checks whose lifetime ran out. Use
        recipient claimed_at for named-list reconciliation.
        Use only the documented status values and comma-separated numeric check_ids.
        Named lists include all recipients; activated_at is the final claim.
      parameters:
        - name: asset
          in: query
          schema:
            type: string
          description: Filter by asset code.
        - name: check_ids
          in: query
          schema:
            type: string
          description: Comma-separated check ids.
        - name: status
          in: query
          schema:
            type: string
            enum: [active, activated, expired]
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
        - name: count
          in: query
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 1000
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    type: object
                    required: [items]
                    properties:
                      items:
                        type: array
                        items:
                          $ref: '#/components/schemas/Check'
        default:
          $ref: '#/components/responses/Error'
  /transfer:
    post:
      tags: [transfers]
      operationId: transfer
      summary: Pay out to a Telegram user
      description: |
        Scope: `payouts`. Rate limit: 30/min per app. Shared payout restrictions apply.

        Provide exactly one non-null recipient: user_id or username. A numeric id
        must belong to an existing available user. A known username settles at once;
        an unknown valid username creates a pending payout held in escrow. Forward
        invite_url to the recipient. On claim it becomes completed; expiry processing
        returns unclaimed funds to the app and sets returned. Read expires_at from
        the response and track final status via getTransfers or opt-in terminal
        webhooks. The public API sends to Telegram recipients, not on-chain addresses.

        spend_id is required. Retrying replays the original response, even if a
        pending payout has since completed. Platform amount limits are expressed
        in USD; handle amount_too_small and amount_too_big. disable_send_notification suppresses only the immediate payout
        notification; a later escrow claim still notifies the recipient.

        Specific errors: `400 unknown_asset`, `400 invalid_request` (missing/both
        recipients, non-positive user_id, invalid field type or length),
        `400 invalid_recipient` (malformed username), `404 user_not_found` (unknown
        numeric id or unavailable/blocked account), `400 invalid_amount`,
        `400 amount_too_small` / `amount_too_big`, `409 insufficient_funds`,
        `409 idempotency_conflict` / `idempotency_in_progress`.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TransferRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    $ref: '#/components/schemas/Transfer'
        default:
          $ref: '#/components/responses/Error'
      parameters:
        - name: user_id
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/TransferRequest/properties/user_id'
        - name: asset
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/TransferRequest/properties/asset'
        - name: amount
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/TransferRequest/properties/amount'
        - name: spend_id
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/TransferRequest/properties/spend_id'
        - name: comment
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/TransferRequest/properties/comment'
        - name: disable_send_notification
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/TransferRequest/properties/disable_send_notification'
        - name: username
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/TransferRequest/properties/username'
  /transferBatch:
    post:
      tags: [transfers]
      operationId: transferBatch
      summary: Batch payouts
      description: |
        Scope: `payouts`. Rate limit: 10/min per app. Shared payout restrictions apply.

        Submit 1..100 JSON items. The whole batch is parsed first: malformed rows
        return `400 invalid_request`, repeated spend_id returns `400 duplicate_spend_id`.
        The payout mode and daily ceiling can reject the whole batch with 403 before
        any row executes. After those checks, items execute independently in order;
        an application error on one row does not undo earlier rows. Each item uses
        the same spend_id namespace as transfer. HTTP 200 / top-level ok=true may
        include failed items: inspect every item's ok and error. Pending username
        payouts are successful items even though they have not yet been claimed.
        Per-item application errors match transfer. An unexpected server failure
        can still abort the call after earlier rows committed; retry with the same
        spend_ids and unchanged item parameters.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    $ref: '#/components/schemas/TransferRequest'
      responses:
        '200':
          description: OK — one entry per input item, in order.
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    type: object
                    required: [items]
                    properties:
                      items:
                        type: array
                        items:
                          $ref: '#/components/schemas/TransferBatchItemResult'
        default:
          $ref: '#/components/responses/Error'
  /getTransfers:
    get:
      tags: [transfers]
      operationId: getTransfers
      summary: List transfers
      description: |
        Scope: `read`. Newest first. `spend_id` is an exact-match filter —
        look up a payout by your own idempotency key.

        Non-numeric transfer_ids entries are ignored; no valid ids returns no rows.
        Only Telegram transfers are listed; on-chain app payouts have a separate webhook payload.
      parameters:
        - name: asset
          in: query
          schema:
            type: string
          description: Filter by asset code.
        - name: transfer_ids
          in: query
          schema:
            type: string
          description: Comma-separated transfer ids.
        - name: spend_id
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, completed, returned]
          description: Case-insensitive transfer state filter.
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
        - name: count
          in: query
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 1000
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    type: object
                    required: [items]
                    properties:
                      items:
                        type: array
                        items:
                          $ref: '#/components/schemas/Transfer'
        default:
          $ref: '#/components/responses/Error'
  /createSubscriptionPlan:
    post:
      tags: [subscriptions]
      operationId: createSubscriptionPlan
      summary: Create a subscription plan
      description: |
        Scope: `subscriptions`. Rate limit: 20/min per app.

        The plan is an **immutable snapshot** — the mandate a user approves
        is exactly this amount per this period; to change the price, create
        a new plan and archive the old one. Users approve via the plan's
        `mini_app_subscribe_url`; the first period is charged at approval.
        Renewals bill automatically; a failed renewal puts the subscription
        into `grace` with retries inside the platform grace window, then
        `expired`.

        Specific errors: `503 subs_disabled` (feature currently unavailable
        platform-side); `400 unknown_asset`; `400 invalid_amount`;
        `409 invalid_period` (below the platform minimum),
        `409 too_many_plans` (active plan limit reached; archive one first).
        Values outside 1..365 return `400 invalid_request` before service validation.
        Creation has no spend_id replay protection.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePlanRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/CreatePlanRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    $ref: '#/components/schemas/Plan'
        default:
          $ref: '#/components/responses/Error'
      parameters:
        - name: name
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreatePlanRequest/properties/name'
        - name: asset
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreatePlanRequest/properties/asset'
        - name: amount
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreatePlanRequest/properties/amount'
        - name: period_days
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/CreatePlanRequest/properties/period_days'
  /getSubscriptionPlans:
    get:
      tags: [subscriptions]
      operationId: getSubscriptionPlans
      summary: List subscription plans
      description: |
        Scope: `read`. Newest first. Each plan carries
        `active_subscribers` — the count of live (`active` + `grace`)
        subscriptions.

        Returns all plans, including archived plans; there are no pagination inputs.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    type: object
                    required: [items]
                    properties:
                      items:
                        type: array
                        items:
                          $ref: '#/components/schemas/PlanWithStats'
        default:
          $ref: '#/components/responses/Error'
  /archiveSubscriptionPlan:
    post:
      tags: [subscriptions]
      operationId: archiveSubscriptionPlan
      summary: Archive a plan
      description: |
        Scope: `subscriptions`.

        Stops **new** approvals; live subscriptions keep renewing on their
        own snapshot (end them per-row via `cancelSubscription`). Archiving
        is irreversible via the API.

        Specific errors: `404 plan_not_found`.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              required: [plan_id]
              properties:
                plan_id:
                  oneOf:
                    - type: integer
                      maximum: 9223372036854775807
                      minimum: 1
                    - type: string
                      pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
                  description: Integer or integer string; parsed value must be within 1..9223372036854775807.
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [plan_id]
              properties:
                plan_id:
                  oneOf:
                    - type: integer
                      maximum: 9223372036854775807
                      minimum: 1
                    - type: string
                      pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
                  description: Integer or integer string; parsed value must be within 1..9223372036854775807.
      responses:
        '200':
          description: 'OK — the updated plan (`archived: true`).'
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    $ref: '#/components/schemas/Plan'
        default:
          $ref: '#/components/responses/Error'
      parameters:
        - name: plan_id
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            oneOf:
              - type: integer
                maximum: 9223372036854775807
                minimum: 1
              - type: string
                pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
            description: Integer or integer string; parsed value must be within 1..9223372036854775807.
  /getSubscriptions:
    get:
      tags: [subscriptions]
      operationId: getSubscriptions
      summary: List subscriptions
      description: |
        Scope: `read`. Newest first. Merchants read `current_period_end` as
        the access deadline.
      parameters:
        - name: plan_id
          in: query
          schema:
            type: integer
          description: Filter by plan.
        - name: user_id
          in: query
          schema:
            type: integer
          description: Filter by subscriber Telegram id.
        - name: status
          in: query
          schema:
            type: string
            enum: [active, grace, cancelled, expired]
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
        - name: count
          in: query
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 500
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    type: object
                    required: [items]
                    properties:
                      items:
                        type: array
                        items:
                          $ref: '#/components/schemas/Subscription'
        default:
          $ref: '#/components/responses/Error'
  /cancelSubscription:
    post:
      tags: [subscriptions]
      operationId: cancelSubscription
      summary: Cancel a subscription
      description: |
        Scope: `subscriptions`.

        Stops renewals (`cancelled_by: "merchant"`). The already-paid
        period stays usable until `current_period_end`; the status flips to
        `cancelled` immediately. The subscriber is notified; the opt-in
        `subscription_cancelled` webhook fires. A cancelled subscriber may
        re-approve later via the plan link.

        Specific errors: `404 sub_not_found` (unknown id / another app's
        subscription); `409 sub_not_active` (already cancelled or expired).
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              required: [subscription_id]
              properties:
                subscription_id:
                  oneOf:
                    - type: integer
                      maximum: 9223372036854775807
                      minimum: 1
                    - type: string
                      pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
                  description: Integer or integer string; parsed value must be within 1..9223372036854775807.
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [subscription_id]
              properties:
                subscription_id:
                  oneOf:
                    - type: integer
                      maximum: 9223372036854775807
                      minimum: 1
                    - type: string
                      pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
                  description: Integer or integer string; parsed value must be within 1..9223372036854775807.
      responses:
        '200':
          description: OK — the updated subscription.
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    $ref: '#/components/schemas/Subscription'
        default:
          $ref: '#/components/responses/Error'
      parameters:
        - name: subscription_id
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            oneOf:
              - type: integer
                maximum: 9223372036854775807
                minimum: 1
              - type: string
                pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
            description: Integer or integer string; parsed value must be within 1..9223372036854775807.
  /updateApp:
    post:
      tags: [app]
      operationId: updateApp
      summary: Update app name and webhook configuration
      description: |
        Scope: `webhooks`. Rate limit: 10/min per app. Returns AppInfo.

        Specific errors: `400 invalid_name` (name resembles a link or handle),
        `400 app_name_reserved` (protected brand), `400 invalid_webhook_events`
        (unknown event name, including invoice_paid). Invalid name length, URL
        or malformed event input returns `400 invalid_request`.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAppRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/UpdateAppRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, result]
                properties:
                  ok:
                    const: true
                  result:
                    $ref: '#/components/schemas/AppInfo'
        default:
          $ref: '#/components/responses/Error'
      parameters:
        - name: name
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/UpdateAppRequest/properties/name'
        - name: webhook_url
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            $ref: '#/components/schemas/UpdateAppRequest/properties/webhook_url'
        - name: webhook_events
          in: query
          required: false
          description: Alternative to this field in the body; body wins. Required-field rules apply after
            merging.
          schema:
            type: string
            description: Comma-separated values. A JSON-encoded array string is also accepted.
webhooks:
  invoice_paid:
    post:
      summary: An invoice was paid (delivered when a webhook URL is configured)
      description: |
        invoice_paid is delivered when the app has a webhook URL. Other event
        types require opt-in via updateApp.webhook_events or the app's settings.

        Verify TgPayCrypto-API-Signature (identical aliases: TgCryptoPay-API-Signature
        and Crypto-Pay-API-Signature): lowercase hex HMAC-SHA256 of the raw request
        bytes, using the raw SHA-256 digest of the app's current primary API token
        as the HMAC key. Restricted tokens do not sign webhooks. Token rotation
        changes the signing key, including for retries of queued deliveries.

        A 2xx response received before the attempt times out acknowledges delivery.
        Respond promptly and process long tasks separately. Failed deliveries are
        retried with increasing delays, up to the platform's attempt limit. Delivery
        is not guaranteed; use getInvoices to reconcile payments after an outage.
        The webhook URL is not automatically disabled on failure. Retries keep update_id and the stored payload, while
        request_date and the signature are regenerated per attempt. Deliveries use
        the current configured URL and do not follow redirects. Dedupe by update_id.
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookUpdate'
                - properties:
                    update_type:
                      const: invoice_paid
                    payload:
                      $ref: '#/components/schemas/Invoice'
        required: true
      responses:
        '200':
          description: Return any 2xx promptly to acknowledge.
      security: []
      parameters:
        - name: TgPayCrypto-API-Signature
          in: header
          required: true
          schema:
            type: string
            pattern: ^[0-9a-f]{64}$
          description: HMAC signature of the raw request bytes; see invoice_paid delivery conventions.
  invoice_expired:
    post:
      summary: An invoice passed its deadline unpaid (opt-in)
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookUpdate'
                - properties:
                    update_type:
                      const: invoice_expired
                    payload:
                      $ref: '#/components/schemas/Invoice'
        required: true
      responses:
        '200':
          description: Return any 2xx promptly to acknowledge.
      security: []
      parameters:
        - name: TgPayCrypto-API-Signature
          in: header
          required: true
          schema:
            type: string
            pattern: ^[0-9a-f]{64}$
          description: HMAC signature of the raw request bytes; see invoice_paid delivery conventions.
  check_activated:
    post:
      summary: A check was claimed (opt-in)
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookUpdate'
                - properties:
                    update_type:
                      const: check_activated
                    payload:
                      $ref: '#/components/schemas/Check'
        required: true
      responses:
        '200':
          description: Return any 2xx promptly to acknowledge.
      description: |
        Single-use check: one claim event. Named list: one event per claimed seat, with activated_slot; recipients is null in this payload. status remains active until the last seat is claimed.
      security: []
      parameters:
        - name: TgPayCrypto-API-Signature
          in: header
          required: true
          schema:
            type: string
            pattern: ^[0-9a-f]{64}$
          description: HMAC signature of the raw request bytes; see invoice_paid delivery conventions.
  refund_completed:
    post:
      summary: A refund was executed (opt-in)
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookUpdate'
                - properties:
                    update_type:
                      const: refund_completed
                    payload:
                      $ref: '#/components/schemas/Invoice'
        required: true
      responses:
        '200':
          description: Return any 2xx promptly to acknowledge.
      security: []
      parameters:
        - name: TgPayCrypto-API-Signature
          in: header
          required: true
          schema:
            type: string
            pattern: ^[0-9a-f]{64}$
          description: HMAC signature of the raw request bytes; see invoice_paid delivery conventions.
  subscription_activated:
    post:
      summary: A user approved a plan (opt-in)
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookUpdate'
                - properties:
                    update_type:
                      const: subscription_activated
                    payload:
                      $ref: '#/components/schemas/Subscription'
        required: true
      responses:
        '200':
          description: Return any 2xx promptly to acknowledge.
      security: []
      parameters:
        - name: TgPayCrypto-API-Signature
          in: header
          required: true
          schema:
            type: string
            pattern: ^[0-9a-f]{64}$
          description: HMAC signature of the raw request bytes; see invoice_paid delivery conventions.
  subscription_charged:
    post:
      summary: A subscription period was billed (opt-in)
      description: |
        `charge.kind` says which period: `initial` (approval charge),
        `renewal`, or `resubscribe` (a cancelled subscriber re-approved).
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookUpdate'
                - properties:
                    update_type:
                      const: subscription_charged
                    payload:
                      allOf:
                        - $ref: '#/components/schemas/Subscription'
                        - type: object
                          properties:
                            charge:
                              $ref: '#/components/schemas/SubscriptionCharge'
                          required: [charge]
        required: true
      responses:
        '200':
          description: Return any 2xx promptly to acknowledge.
      security: []
      parameters:
        - name: TgPayCrypto-API-Signature
          in: header
          required: true
          schema:
            type: string
            pattern: ^[0-9a-f]{64}$
          description: HMAC signature of the raw request bytes; see invoice_paid delivery conventions.
  subscription_cancelled:
    post:
      summary: A subscription was cancelled by either side (opt-in)
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookUpdate'
                - properties:
                    update_type:
                      const: subscription_cancelled
                    payload:
                      $ref: '#/components/schemas/Subscription'
        required: true
      responses:
        '200':
          description: Return any 2xx promptly to acknowledge.
      security: []
      parameters:
        - name: TgPayCrypto-API-Signature
          in: header
          required: true
          schema:
            type: string
            pattern: ^[0-9a-f]{64}$
          description: HMAC signature of the raw request bytes; see invoice_paid delivery conventions.
  subscription_expired:
    post:
      summary: The grace window ran out unpaid (opt-in)
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookUpdate'
                - properties:
                    update_type:
                      const: subscription_expired
                    payload:
                      $ref: '#/components/schemas/Subscription'
        required: true
      responses:
        '200':
          description: Return any 2xx promptly to acknowledge.
      security: []
      parameters:
        - name: TgPayCrypto-API-Signature
          in: header
          required: true
          schema:
            type: string
            pattern: ^[0-9a-f]{64}$
          description: HMAC signature of the raw request bytes; see invoice_paid delivery conventions.
  transfer_completed:
    post:
      summary: A payout completed (opt-in)
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookUpdate'
                - properties:
                    update_type:
                      const: transfer_completed
                    payload:
                      oneOf:
                        - allOf:
                            - $ref: '#/components/schemas/Transfer'
                            - properties:
                                status:
                                  const: completed
                              not:
                                required: [kind]
                        - allOf:
                            - $ref: '#/components/schemas/WithdrawalPayoutWebhook'
                            - properties:
                                status:
                                  const: completed
        required: true
      responses:
        '200':
          description: Return any 2xx promptly to acknowledge.
      security: []
      parameters:
        - name: TgPayCrypto-API-Signature
          in: header
          required: true
          schema:
            type: string
            pattern: ^[0-9a-f]{64}$
          description: HMAC signature of the raw request bytes; see invoice_paid delivery conventions.
      description: |
        Payload is a Transfer for Telegram payouts, or WithdrawalPayoutWebhook (kind=withdrawal) for on-chain payouts created through the app. The latter are absent from getTransfers.
  transfer_returned:
    post:
      summary: A payout returned to the app (opt-in)
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookUpdate'
                - properties:
                    update_type:
                      const: transfer_returned
                    payload:
                      oneOf:
                        - allOf:
                            - $ref: '#/components/schemas/Transfer'
                            - properties:
                                status:
                                  const: returned
                              not:
                                required: [kind]
                        - allOf:
                            - $ref: '#/components/schemas/WithdrawalPayoutWebhook'
                            - properties:
                                status:
                                  const: failed
        required: true
      responses:
        '200':
          description: Return any 2xx promptly to acknowledge.
      security: []
      parameters:
        - name: TgPayCrypto-API-Signature
          in: header
          required: true
          schema:
            type: string
            pattern: ^[0-9a-f]{64}$
          description: HMAC signature of the raw request bytes; see invoice_paid delivery conventions.
      description: |
        Payload is a Transfer for Telegram payouts, or WithdrawalPayoutWebhook (kind=withdrawal) for on-chain payouts created through the app. The latter are absent from getTransfers.
  check_expired:
    post:
      summary: A named-recipient check expired and its unclaimed funds returned (opt-in)
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookUpdate'
                - properties:
                    update_type:
                      const: check_expired
                    payload:
                      $ref: '#/components/schemas/Check'
        required: true
      responses:
        '200':
          description: Return any 2xx promptly to acknowledge.
      description: |
        Currently emitted only for named-recipient checks. recipients includes every seat and claimed_at. The current serializer reports status=active even though expiry has returned unclaimed funds; use update_type and expires_at. Ordinary check expiry does not emit this event.
      security: []
      parameters:
        - name: TgPayCrypto-API-Signature
          in: header
          required: true
          schema:
            type: string
            pattern: ^[0-9a-f]{64}$
          description: HMAC signature of the raw request bytes; see invoice_paid delivery conventions.
components:
  securitySchemes:
    apiToken:
      type: apiKey
      in: header
      name: TgPayCrypto-API-Token
      description: The canonical token header. Shown once at app creation / rotation.
    apiTokenLegacy:
      type: apiKey
      in: header
      name: TgCryptoPay-API-Token
      description: The canonical header's name before 2026-09-07 — still accepted, so integrations built
        earlier keep working unchanged.
    apiTokenCompat:
      type: apiKey
      in: header
      name: Crypto-Pay-API-Token
      description: Accepted token header alias. The first non-empty canonical/legacy/compatibility header
        wins, in that order.
  responses:
    Error:
      description: |
        Error — `{"ok": false, "error": {"code": <HTTP status>, "name":
        "<machine_name>"}}` with the same HTTP status on the wire. See the
        operation description for its specific error names and the API
        description for the common ones.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  schemas:
    Amount:
      type: string
      description: Non-negative fixed-point decimal string in whole-coin (or fiat) units, including zero
        balances and fees. Never a JSON number.
      examples: ['10.5']
      pattern: ^[0-9]+(?:\.[0-9]+)?$
    AmountMinor:
      type: string
      description: 'Extension: the integer minor-unit amount as a string (wei-scale values overflow the
        JS Number type).'
      examples: ['10500000']
      pattern: ^[0-9]+$
    ErrorEnvelope:
      type: object
      required: [ok, error]
      properties:
        ok:
          const: false
        error:
          type: object
          required: [code, name]
          properties:
            code:
              type: integer
              description: Duplicates the HTTP status (400/401/403/404/409/429/503/500).
            name:
              type: string
              description: Machine-readable error name to branch on, e.g. "insufficient_funds".
    AppInfo:
      type: object
      required: [app_id, name, payment_processing_bot_username, webhook_url, webhook_events, scopes, token_name,
        daily_payout_max_usd_cents, daily_payout_spent_usd_cents, payout_mode]
      properties:
        app_id:
          type: string
          description: The app public id, serialized as a string.
        name:
          type: string
        payment_processing_bot_username:
          type: string
          description: The Telegram bot whose Mini App processes payments.
        webhook_url:
          type: [string, 'null']
        webhook_events:
          type: array
          items:
            type: string
            enum: [invoice_expired, check_activated, refund_completed, subscription_activated, subscription_charged,
              subscription_cancelled, subscription_expired, transfer_completed, transfer_returned, check_expired]
          description: Selected extended webhook update types. invoice_paid is automatic when a URL is
            configured and is not listed here.
        scopes:
          type: [array, 'null']
          items:
            type: string
            enum: [read, invoices, refunds, payouts, checks, subscriptions, webhooks]
          description: 'Extension: this token''s scopes, sorted; null = full-access primary token.'
        token_name:
          type: [string, 'null']
          description: 'Extension: the restricted token''s label; null for the primary token.'
        daily_payout_max_usd_cents:
          type: integer
          minimum: 0
          description: Owner-set rolling 24-hour payout ceiling in USD cents; 0 means no ceiling.
        daily_payout_spent_usd_cents:
          type: integer
          minimum: 0
          description: Spend currently counted toward the owner's rolling 24-hour payout limit, in USD
            cents. Includes transfers, checks and app withdrawals; invoice refunds are excluded.
        payout_mode:
          type: string
          enum: ['off', allowlist, any]
          description: off disables API payouts; allowlist permits only owner-listed recipients; any permits
            any valid recipient, including bearer checks.
    BalanceRow:
      type: object
      required: [currency_code, available, onhold, amount_minor]
      properties:
        currency_code:
          type: string
          examples: [USDT]
        available:
          $ref: '#/components/schemas/Amount'
        onhold:
          allOf:
            - $ref: '#/components/schemas/Amount'
          description: Funds held in unclaimed app checks and pending username transfers; not spendable.
        amount_minor:
          $ref: '#/components/schemas/AmountMinor'
    Currency:
      type: object
      required: [code, name, decimals, is_blockchain, is_stablecoin, is_fiat]
      properties:
        code:
          type: string
        name:
          type: string
          examples: [Tether USD]
        decimals:
          type: integer
          description: 'Minor-unit exponent (crypto: per asset, up to 18; fiat: always 2).'
        is_blockchain:
          type: boolean
        is_stablecoin:
          type: boolean
        is_fiat:
          type: boolean
    ExchangeRate:
      type: object
      required: [is_valid, is_crypto, is_fiat, source, target, rate]
      properties:
        is_valid:
          type: boolean
          description: A true value does not guarantee that the quote is current.
        is_crypto:
          type: boolean
        is_fiat:
          type: boolean
        source:
          type: string
          description: Crypto asset code.
        target:
          type: string
          description: Fiat code.
        rate:
          type: string
          description: Fiat per 1 whole unit of source, as a fixed-point decimal string.
    AppStats:
      type: object
      properties:
        volume:
          type: number
          description: USD value of invoices paid in the window, at current prices (best-effort).
        conversion:
          type: number
          description: Paid-in-window / created-in-window percentage, rounded to 2 decimal places; 0 when
            no invoices were created. May exceed 100 if older invoices are paid in the window.
        unique_users_count:
          type: integer
          description: Distinct recorded payers of invoices paid in the window, including anonymous payments
            in this aggregate count.
        created_invoice_count:
          type: integer
        paid_invoice_count:
          type: integer
        start_at:
          type: string
          format: date-time
        end_at:
          type: string
          format: date-time
      required: [volume, conversion, unique_users_count, created_invoice_count, paid_invoice_count, start_at,
        end_at]
    CreateInvoiceRequest:
      type: object
      properties:
        currency_type:
          anyOf:
            - type: string
              enum: [crypto, fiat]
              default: crypto
            - type: 'null'
        asset:
          anyOf:
            - type: string
              description: Required in crypto mode; non-empty asset is rejected in fiat mode.
              maxLength: 16
            - type: 'null'
        fiat:
          anyOf:
            - type: string
              description: 'Fiat mode: the pricing fiat (is_fiat rows of getCurrencies).'
              maxLength: 8
            - type: 'null'
        accepted_assets:
          anyOf:
            - description: 'Fiat mode: comma-separated asset codes or a JSON array. Omitted, null or empty
                list means all enabled assets.'
              oneOf:
                - type: string
                  examples: ['USDT,GRAM']
                - type: array
                  items:
                    type: string
            - type: 'null'
        amount:
          anyOf:
            - $ref: '#/components/schemas/AmountInput'
              description: 'Required unless open_amount. Crypto mode: in asset units; fiat mode: in fiat
                units, max 2 decimal places.'
            - type: 'null'
        open_amount:
          anyOf:
            - $ref: '#/components/schemas/BooleanInput'
              description: 'Extension, crypto mode only: no fixed amount — the payer enters it at pay
                time. Mutually exclusive with amount.'
              default: false
            - type: 'null'
        description:
          anyOf:
            - type: string
              maxLength: 1024
              description: Shown to the payer.
            - type: 'null'
        hidden_message:
          anyOf:
            - type: string
              maxLength: 2048
              description: Shown to the payer only after payment.
            - type: 'null'
        payload:
          anyOf:
            - type: string
              maxLength: 4096
              description: Opaque merchant data echoed back in the invoice object and webhook.
            - type: 'null'
        allow_comments:
          anyOf:
            - $ref: '#/components/schemas/BooleanInput'
              default: true
            - type: 'null'
        allow_anonymous:
          anyOf:
            - $ref: '#/components/schemas/BooleanInput'
              default: true
            - type: 'null'
        paid_btn_name:
          anyOf:
            - type: string
              enum: [viewItem, openChannel, openBot, callback]
              description: Post-payment CTA button.
            - type: 'null'
        paid_btn_url:
          anyOf:
            - type: string
              maxLength: 512
              description: Required with paid_btn_name; must begin with lowercase http:// or https://.
                Without paid_btn_name it is ignored.
            - type: 'null'
        swap_to:
          anyOf:
            - type: string
              description: Auto-swap the received amount to this asset after payment, best-effort — unavailable
                pricing, tradability or capacity skips conversion and keeps the paid asset.
              maxLength: 16
            - type: 'null'
        expires_in:
          anyOf:
            - oneOf:
                - type: integer
                  maximum: 2678400
                  minimum: 0
                - type: string
                  pattern: ^\s*(?:\+?0*(?:[1-9][0-9]{0,5}|1[0-9]{6}|2[0-5][0-9]{5}|26[0-6][0-9]{4}|267[0-7][0-9]{3}|2678[0-3][0-9]{2}|2678400)|[+-]?0+)\s*$
              description: Seconds until expiry, at most 31 days. Omitted, null or zero uses the default
                lifetime. Read expiration_date in the response for the actual deadline; null means no deadline.
            - type: 'null'
        rate_lock_seconds:
          anyOf:
            - oneOf:
                - type: integer
                  maximum: 86400
                  minimum: 0
                - type: string
                  pattern: ^\s*(?:\+?0*(?:[1-9][0-9]{0,3}|[1-7][0-9]{4}|8[0-5][0-9]{3}|86[0-3][0-9]{2}|86400)|[+-]?0+)\s*$
              description: 'Fiat mode: positive value requests a rate lock, clamped to the platform window
                (minimum requested window 60 s). Zero/omitted/null disables the lock; expired locks revert
                to fresh rates.'
            - type: 'null'
      allOf:
        - if:
            required: [currency_type]
            properties:
              currency_type:
                const: fiat
          then:
            allOf:
              - required: [fiat]
                properties:
                  fiat:
                    not:
                      type: 'null'
              - required: [amount]
                properties:
                  amount:
                    not:
                      type: 'null'
            properties:
              asset:
                enum: [null, '']
              open_amount:
                not:
                  oneOf:
                    - const: true
                    - const: 1
                    - type: string
                      pattern: ^\s*(?:[Tt][Rr][Uu][Ee]|1|[Yy][Ee][Ss]|[Oo][Nn])\s*$
              fiat:
                type: string
                minLength: 1
          else:
            allOf:
              - required: [asset]
                properties:
                  asset:
                    not:
                      type: 'null'
              - if:
                  required: [open_amount]
                  properties:
                    open_amount:
                      oneOf:
                        - const: true
                        - const: 1
                        - type: string
                          pattern: ^\s*(?:[Tt][Rr][Uu][Ee]|1|[Yy][Ee][Ss]|[Oo][Nn])\s*$
                then:
                  properties:
                    amount:
                      type: 'null'
                else:
                  required: [amount]
                  properties:
                    amount:
                      not:
                        type: 'null'
            properties:
              asset:
                type: string
                minLength: 1
    Invoice:
      type: object
      description: The invoice object — API results and invoice webhook payloads.
      properties:
        invoice_id:
          type: integer
        hash:
          type: string
          description: Public id (also inside pay_url).
        status:
          type: string
          enum: [active, paid, expired]
        currency_type:
          type: string
          enum: [crypto, fiat]
        asset:
          type: [string, 'null']
          description: 'Crypto invoice: the fixed asset. Fiat invoice: always null here (see paid_asset).'
        fiat:
          type: [string, 'null']
        accepted_assets:
          type: [array, 'null']
          items:
            type: string
          description: 'Fiat invoices: the concrete list the payer may pay with; null for crypto invoices.'
        amount:
          type: [string, 'null']
          description: 'Face amount: fiat units for fiat invoices, crypto units otherwise. Null for an
            unpaid open-amount invoice.'
        amount_minor:
          type: [string, 'null']
          description: 'Extension: crypto amount in minor units (string); null while a fiat/open-amount
            invoice is unpaid.'
        paid_asset:
          type: [string, 'null']
          description: 'Fiat invoices, once paid: the asset the payer chose.'
        paid_amount:
          type: [string, 'null']
          description: 'Fiat invoices, once paid: the crypto amount actually paid.'
        paid_fiat_rate:
          type: [string, 'null']
          description: 'Fiat invoices, once paid: the fiat-per-asset rate used.'
        paid_usd_rate:
          type: [string, 'null']
          description: USD price of the paid asset stamped at pay time.
        fee_asset:
          type: [string, 'null']
        fee_amount:
          type: [string, 'null']
          description: Platform fee, charged to the merchant's leg — the authoritative number for reconciliation.
        fee:
          type: [string, 'null']
          deprecated: true
          description: Deprecated alias of fee_amount.
        usd_rate:
          type: [string, 'null']
          deprecated: true
          description: Deprecated alias of paid_usd_rate.
        description:
          type: [string, 'null']
        hidden_message:
          type: [string, 'null']
        payload:
          type: [string, 'null']
        allow_comments:
          type: boolean
        allow_anonymous:
          type: boolean
        comment:
          type: [string, 'null']
          description: The payer's comment (only if allowed).
        paid_anonymously:
          type: boolean
        is_anonymous:
          type: boolean
          description: Alias of paid_anonymously.
        paid_btn_name:
          type: [string, 'null']
        paid_btn_url:
          type: [string, 'null']
        swap_to:
          type: [string, 'null']
        is_swapped:
          type: boolean
        swapped_uid:
          type: [string, 'null']
        swapped_to:
          type: [string, 'null']
        swapped_rate:
          type: [string, 'null']
        swapped_output:
          type: [string, 'null']
        swapped_usd_rate:
          type: [string, 'null']
        swapped_usd_amount:
          type: [string, 'null']
        pay_url:
          type: string
          description: t.me deep link the payer opens.
        bot_invoice_url:
          type: string
          description: Alias of pay_url.
        mini_app_invoice_url:
          type: string
          description: Alias of pay_url.
        web_app_invoice_url:
          type: string
          description: Alias of pay_url.
        paid_by_user_id:
          type: [integer, 'null']
          description: The payer's Telegram id — null when the payer paid anonymously.
        open_amount:
          type: boolean
          description: 'Extension: this is an open-amount (donation) invoice.'
        created_at:
          type: string
          format: date-time
        paid_at:
          type: [string, 'null']
          format: date-time
        refunded_amount:
          type: [string, 'null']
          description: Cumulative refunded crypto amount in the settlement asset, including for fiat-priced
            invoices.
        refunded_minor:
          type: [string, 'null']
          description: 'Extension: the same in minor units (string).'
        refunded_at:
          type: [string, 'null']
          format: date-time
          description: 'Extension: stamped once fully refunded (status stays paid).'
        rate_lock_until:
          type: [string, 'null']
          format: date-time
          description: 'Extension: rate-lock deadline; null when no lock was requested.'
        rate_lock_rates:
          type: [object, 'null']
          additionalProperties:
            type: string
          description: 'Extension: {asset_code: rate_string} stamped at creation — the exact rates quoted
            while the lock is live.'
        expiration_date:
          type: [string, 'null']
          format: date-time
        expires_at:
          type: [string, 'null']
          format: date-time
          description: Alias of expiration_date.
      required: [invoice_id, hash, status, currency_type, asset, fiat, accepted_assets, amount, amount_minor,
        paid_asset, paid_amount, paid_fiat_rate, paid_usd_rate, fee_asset, fee_amount, fee, usd_rate,
        description, hidden_message, payload, allow_comments, allow_anonymous, comment, paid_anonymously,
        is_anonymous, paid_btn_name, paid_btn_url, swap_to, is_swapped, swapped_uid, swapped_to, swapped_rate,
        swapped_output, swapped_usd_rate, swapped_usd_amount, pay_url, bot_invoice_url, mini_app_invoice_url,
        web_app_invoice_url, paid_by_user_id, open_amount, created_at, paid_at, refunded_amount, refunded_minor,
        refunded_at, rate_lock_until, rate_lock_rates, expiration_date, expires_at]
    RefundInvoiceRequest:
      type: object
      required: [invoice_id]
      properties:
        invoice_id:
          oneOf:
            - type: integer
              maximum: 9223372036854775807
              minimum: 1
            - type: string
              pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
          description: Integer or integer string; parsed value must be within 1..9223372036854775807.
        amount:
          anyOf:
            - $ref: '#/components/schemas/AmountInput'
              description: Refund in the crypto asset used to settle the invoice (paid_asset for fiat
                invoices), never fiat units. Omitted/null means the full unrefunded crypto remainder;
                partial refunds accumulate.
            - type: 'null'
        spend_id:
          anyOf:
            - type: string
              maxLength: 64
              description: Optional replay key; empty/null disables idempotency. Recommended for retrying
                refunds.
            - type: 'null'
    CreateCheckRequest:
      type: object
      required: [asset]
      properties:
        asset:
          type: string
          maxLength: 16
        amount:
          anyOf:
            - $ref: '#/components/schemas/AmountInput'
            - type: 'null'
        pin_to_user_id:
          anyOf:
            - oneOf:
                - type: integer
                  maximum: 9223372036854775807
                  minimum: 1
                - type: string
                  pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
              description: Restrict claiming to this Telegram user id.
            - type: 'null'
        pin_to_username:
          anyOf:
            - type: string
              maxLength: 64
              description: Restrict by @username (leading @ optional, case-insensitive); ignored when
                pin_to_user_id is also given. Must belong to an existing user of the platform.
            - type: 'null'
        spend_id:
          anyOf:
            - type: string
              maxLength: 64
              description: Optional replay key; empty/null disables idempotency.
            - type: 'null'
        recipients:
          anyOf:
            - type: array
              minItems: 1
              maxItems: 100
              items:
                $ref: '#/components/schemas/CheckRecipientRequest'
              description: JSON body only. One seat per recipient; sum funds the check. Mutually exclusive
                with pins.
            - type: 'null'
        password:
          anyOf:
            - type: string
              maxLength: 64
              description: Optional claim password; empty/null means no password. Never returned.
            - type: 'null'
        expires_in:
          anyOf:
            - oneOf:
                - type: integer
                  maximum: 9223372036854775807
                  minimum: 0
                - type: string
                  pattern: ^\s*(?:\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)|[+-]?0+)\s*$
              description: Seconds until expiry; zero, null or omitted means no explicit deadline. Choose
                a duration that produces a representable expiry timestamp.
            - type: 'null'
      anyOf:
        - required: [amount]
          properties:
            amount:
              not:
                type: 'null'
        - required: [recipients]
          properties:
            recipients:
              not:
                type: 'null'
      allOf:
        - if:
            required: [recipients]
            properties:
              recipients:
                not:
                  type: 'null'
          then:
            properties:
              pin_to_user_id:
                type: 'null'
              pin_to_username:
                enum: [null, '']
    Check:
      type: object
      properties:
        check_id:
          type: integer
        hash:
          type: string
          description: Public id.
        asset:
          type: string
        amount:
          $ref: '#/components/schemas/Amount'
          description: Original funded total for a named list, or the single-use amount; not the unclaimed
            remainder.
        amount_minor:
          $ref: '#/components/schemas/AmountMinor'
        bot_check_url:
          type: string
          description: t.me claim deep link.
        status:
          type: string
          enum: [active, activated, expired]
          description: activated once fully claimed; expired (extension) once the lifetime ran out and the
            unclaimed remainder returned to the app; otherwise active — still claimable.
        pin_to_user_id:
          type: [integer, 'null']
        created_at:
          type: string
          format: date-time
        activated_at:
          type: [string, 'null']
          format: date-time
          description: Time of the last claim once fully activated; null before then.
        expires_at:
          type: [string, 'null']
          format: date-time
        recipients:
          type: [array, 'null']
          items:
            $ref: '#/components/schemas/CheckRecipient'
          description: Named seats on create/getChecks and check_expired; null for ordinary checks and
            check_activated payloads.
        activated_slot:
          allOf:
            - $ref: '#/components/schemas/CheckRecipient'
          description: Present only on a named-seat check_activated webhook.
      required: [check_id, hash, asset, amount, amount_minor, bot_check_url, status, pin_to_user_id, created_at,
        activated_at, expires_at, recipients]
    TransferRequest:
      type: object
      required: [asset, amount, spend_id]
      properties:
        user_id:
          anyOf:
            - oneOf:
                - type: integer
                  maximum: 9223372036854775807
                  minimum: 1
                - type: string
                  pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
              description: Recipient's Telegram user id.
            - type: 'null'
        asset:
          type: string
          minLength: 2
          maxLength: 16
        amount:
          $ref: '#/components/schemas/AmountInput'
          description: Also subject to platform min/max USD bounds.
        spend_id:
          type: string
          minLength: 1
          maxLength: 64
          description: Required idempotency key, unique per app.
        comment:
          anyOf:
            - type: string
              maxLength: 1024
              description: Shown to the recipient.
            - type: 'null'
        disable_send_notification:
          anyOf:
            - $ref: '#/components/schemas/BooleanInput'
              description: Suppresses the immediate recipient notification only. A later escrow claim
                still sends a notification.
              default: false
            - type: 'null'
        username:
          anyOf:
            - type: string
              maxLength: 64
              description: 'Leading @ optional, case-insensitive. Normalized username: a letter followed
                by 3..31 letters, digits or underscores. Unknown username creates pending escrow.'
            - type: 'null'
      oneOf:
        - allOf:
            - required: [user_id]
              properties:
                user_id:
                  not:
                    type: 'null'
          properties:
            username:
              enum: [null, '']
        - allOf:
            - required: [username]
              properties:
                username:
                  not:
                    type: 'null'
          properties:
            user_id:
              type: 'null'
            username:
              type: string
              minLength: 1
    Transfer:
      type: object
      properties:
        transfer_id:
          type: integer
        hash:
          type: string
        user_id:
          type: [integer, 'null']
          description: Recipient id; null until a pending username payout is claimed, and on an unclaimed
            returned payout.
        asset:
          type: string
        amount:
          $ref: '#/components/schemas/Amount'
        amount_minor:
          $ref: '#/components/schemas/AmountMinor'
        spend_id:
          type: string
        comment:
          type: [string, 'null']
        status:
          type: string
          enum: [pending, completed, returned]
        created_at:
          type: string
          format: date-time
        completed_at:
          type: [string, 'null']
          format: date-time
        recipient_username:
          type: [string, 'null']
          description: Normalized username for an originally pending payout, retained after claim/return.
            Null on immediate payouts.
        expires_at:
          type: [string, 'null']
          format: date-time
          description: Escrow expiry deadline; null on immediate payouts. Retained after claim/return.
        invite_url:
          type: [string, 'null']
          description: Bot invitation link on pending API results; null on terminal results/webhooks.
      required: [transfer_id, hash, user_id, asset, amount, amount_minor, spend_id, comment, status, created_at,
        completed_at, recipient_username, expires_at, invite_url]
    TransferBatchItemResult:
      type: object
      required: [ok, spend_id]
      properties:
        ok:
          type: boolean
        spend_id:
          type: string
        result:
          allOf:
            - $ref: '#/components/schemas/Transfer'
          description: Present when ok is true.
        error:
          type: object
          description: Present when ok is false — same error names as single transfer.
          properties:
            code:
              type: integer
            name:
              type: string
          required: [code, name]
      oneOf:
        - properties:
            ok:
              const: true
          required: [result]
          not:
            required: [error]
        - properties:
            ok:
              const: false
          required: [error]
          not:
            required: [result]
    CreatePlanRequest:
      type: object
      required: [name, asset, amount, period_days]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 64
        asset:
          type: string
          maxLength: 16
        amount:
          $ref: '#/components/schemas/AmountInput'
          description: The per-period charge.
        period_days:
          oneOf:
            - type: integer
              maximum: 365
              minimum: 1
            - type: string
              pattern: ^\s*\+?0*(?:[1-9][0-9]{0,1}|[1-2][0-9]{2}|3[0-5][0-9]{1}|36[0-4]|365)\s*$
          description: Additionally bounded server-side to the platform minimum — outside returns 409
            invalid_period.
    Plan:
      type: object
      properties:
        plan_id:
          type: integer
        name:
          type: string
        asset:
          type: string
        amount:
          $ref: '#/components/schemas/Amount'
        amount_minor:
          $ref: '#/components/schemas/AmountMinor'
        period_days:
          type: integer
        archived:
          type: boolean
          description: Archived plans accept no new approvals.
        mini_app_subscribe_url:
          type: string
          description: t.me deep link users open to approve.
        created_at:
          type: string
          format: date-time
      required: [plan_id, name, asset, amount, amount_minor, period_days, archived, mini_app_subscribe_url,
        created_at]
    PlanWithStats:
      allOf:
        - $ref: '#/components/schemas/Plan'
        - type: object
          properties:
            active_subscribers:
              type: integer
              description: Count of live (active + grace) subscriptions on the plan.
          required: [active_subscribers]
    Subscription:
      type: object
      properties:
        subscription_id:
          type: integer
        plan_id:
          type: integer
        user_id:
          type: integer
          description: Subscriber's Telegram id.
        asset:
          type: string
          description: Plan snapshot.
        amount:
          allOf:
            - $ref: '#/components/schemas/Amount'
          description: Snapshot per-period amount.
        amount_minor:
          $ref: '#/components/schemas/AmountMinor'
        period_days:
          type: integer
        status:
          type: string
          enum: [active, grace, cancelled, expired]
          description: grace = renewal failed, retrying inside the grace window.
        auto_renew:
          type: boolean
        period_no:
          type: integer
          description: Number of paid periods so far.
        current_period_start:
          type: string
          format: date-time
        current_period_end:
          type: string
          format: date-time
          description: The paid-through deadline — read this as the access deadline.
        created_at:
          type: string
          format: date-time
        cancelled_at:
          type: [string, 'null']
          format: date-time
        cancelled_by:
          type: [string, 'null']
          enum: [user, merchant, null]
        expired_at:
          type: [string, 'null']
          format: date-time
      required: [subscription_id, plan_id, user_id, asset, amount, amount_minor, period_days, status,
        auto_renew, period_no, current_period_start, current_period_end, created_at, cancelled_at, cancelled_by,
        expired_at]
    SubscriptionCharge:
      type: object
      description: The charge sub-object attached to subscription_charged webhook payloads.
      properties:
        period_no:
          type: integer
        kind:
          type: string
          enum: [initial, renewal, resubscribe]
        asset:
          type: string
        amount:
          $ref: '#/components/schemas/Amount'
        fee:
          $ref: '#/components/schemas/Amount'
        paid_at:
          type: string
          format: date-time
      required: [period_no, kind, asset, amount, fee, paid_at]
    WebhookUpdate:
      type: object
      required: [update_id, update_type, request_date, payload]
      properties:
        update_id:
          type: integer
          description: Stable across redeliveries of the same event — dedupe on it.
        update_type:
          type: string
          enum: [invoice_paid, invoice_expired, check_activated, refund_completed, subscription_activated,
            subscription_charged, subscription_cancelled, subscription_expired, transfer_completed, transfer_returned,
            check_expired]
        request_date:
          type: string
          format: date-time
          description: Stamped per delivery attempt — UTC, ISO 8601 with microseconds (2026-08-11T12:00:00.123456Z).
        payload:
          description: The full object for the event type.
    UpdateAppRequest:
      type: object
      description: Omitted or null fields leave their settings unchanged. Empty object is a no-op.
      properties:
        name:
          anyOf:
            - type: string
              description: Trimmed app name. Link/handle-shaped names and protected brand names are rejected.
              minLength: 1
              maxLength: 64
            - type: 'null'
        webhook_url:
          anyOf:
            - type: string
              description: Absolute public HTTPS URL, no embedded credentials; empty string clears it.
                Redirects are not followed.
              maxLength: 256
            - type: 'null'
        webhook_events:
          oneOf:
            - type: array
              items:
                type: string
                enum: [invoice_expired, check_activated, refund_completed, subscription_activated, subscription_charged,
                  subscription_cancelled, subscription_expired, transfer_completed, transfer_returned,
                  check_expired]
            - type: string
              description: Comma-separated or JSON-encoded array of extended event names. Empty string
                clears the selection.
            - type: 'null'
          description: Replace the extended-event selection; [] clears it. invoice_paid is automatic when
            a URL is set and must not be included.
    CheckRecipientRequest:
      type: object
      required: [amount]
      properties:
        user_id:
          anyOf:
            - oneOf:
                - type: integer
                  maximum: 9223372036854775807
                  minimum: 1
                - type: string
                  pattern: ^\s*\+?0*(?:[1-9][0-9]{0,17}|[1-8][0-9]{18}|9[0-1][0-9]{17}|92[0-1][0-9]{16}|922[0-2][0-9]{15}|9223[0-2][0-9]{14}|92233[0-6][0-9]{13}|922337[0-1][0-9]{12}|92233720[0-2][0-9]{10}|922337203[0-5][0-9]{9}|9223372036[0-7][0-9]{8}|92233720368[0-4][0-9]{7}|922337203685[0-3][0-9]{6}|9223372036854[0-6][0-9]{5}|92233720368547[0-6][0-9]{4}|922337203685477[0-4][0-9]{3}|9223372036854775[0-7][0-9]{2}|922337203685477580[0-6]|9223372036854775807)\s*$
              description: Integer or integer string; parsed value must be within 1..9223372036854775807.
            - type: 'null'
        username:
          anyOf:
            - type: string
              maxLength: 64
              description: Optional leading @, case-insensitive; normalized handle must start with a letter
                and contain 4..32 letters, digits or underscores.
            - type: 'null'
        amount:
          $ref: '#/components/schemas/AmountInput'
      oneOf:
        - allOf:
            - required: [user_id]
              properties:
                user_id:
                  not:
                    type: 'null'
          properties:
            username:
              enum: [null, '']
        - allOf:
            - required: [username]
              properties:
                username:
                  not:
                    type: 'null'
          properties:
            user_id:
              type: 'null'
            username:
              type: string
              minLength: 1
    CheckRecipient:
      type: object
      properties:
        user_id:
          type: [integer, 'null']
        username:
          type: [string, 'null']
        amount:
          $ref: '#/components/schemas/Amount'
        amount_minor:
          $ref: '#/components/schemas/AmountMinor'
        claimed_at:
          type: [string, 'null']
          format: date-time
      required: [user_id, username, amount, amount_minor, claimed_at]
    AmountInput:
      type: string
      minLength: 1
      maxLength: 40
      description: Positive amount as a decimal string (not a JSON number). Surrounding whitespace and
        exponent notation are accepted. Crypto rounds to asset precision, midpoint away from zero, and
        must remain positive; fiat requires exact positive cents. Values must fit the server decimal parser
        and scaling arithmetic.
      pattern: ^\s*\+?(?=[0-9.]*[1-9])(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?\s*$
    BooleanInput:
      oneOf:
        - type: boolean
        - type: integer
          enum: [0, 1]
        - type: string
          pattern: ^\s*(?:[Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee]|[01]|[Yy][Ee][Ss]|[Nn][Oo]|[Oo][Nn]|[Oo][Ff][Ff])\s*$
    WithdrawalPayoutWebhook:
      type: object
      properties:
        transfer_id:
          type: integer
          description: Numeric withdrawal id; a separate id namespace from user transfers. Use kind and
            hash to distinguish.
        hash:
          type: string
        user_id:
          type: 'null'
        asset:
          type: string
        amount:
          $ref: '#/components/schemas/Amount'
        amount_minor:
          $ref: '#/components/schemas/AmountMinor'
        spend_id:
          type: string
        comment:
          type: [string, 'null']
        status:
          type: string
          enum: [completed, failed]
          description: completed on transfer_completed; failed (not returned) on transfer_returned.
        created_at:
          type: string
          format: date-time
        completed_at:
          type: [string, 'null']
          format: date-time
        recipient_username:
          type: 'null'
        expires_at:
          type: 'null'
        invite_url:
          type: 'null'
        kind:
          type: string
          const: withdrawal
        fee_minor:
          $ref: '#/components/schemas/AmountMinor'
        network:
          type: string
        address:
          type: string
        txid:
          type: [string, 'null']
        withdrawal_id:
          type: string
          format: uuid
        withdrawal_status:
          type: string
          enum: [confirmed, failed, rejected, cancelled]
        error:
          type: [string, 'null']
          description: Failure detail when status=failed; null on completion.
      required: [transfer_id, hash, user_id, asset, amount, amount_minor, spend_id, comment, status, created_at,
        completed_at, recipient_username, expires_at, invite_url, kind, fee_minor, network, address, txid,
        withdrawal_id, withdrawal_status, error]
      description: Webhook-only on-chain payout variant, created through the app. transfer/transferBatch
        cannot create it and getTransfers does not list it. Distinguish it by kind=withdrawal.
