# NETTS API v2 — OpenAPI 3.1
#
# Источник истины для API Reference на doc.netts.io (Milestone 1, 05.09.2026).
# Собрана из существующих страниц api/v2/endpoints/*.md; страницы пока остаются
# на месте и служат эталоном для вычитки — см. openapi/README.md.
#
# Соглашения, которые пришлось отразить честно, а не причесать:
#   * Единого формата ошибок у API НЕТ. Исторически сложились четыре:
#       {"detail": "текст"}                        — order1h/order5m, 401
#       {"detail": {"code": -1, "msg": "..."}}     — pricing, 401/404 (FastAPI)
#       {"code": 1004, "msg": "..."}               — order1h/order5m, 4xx/5xx
#       {"status": "error", "message": "..."}      — userinfo/prices
#       {"success": false, "error": {...}}         — pricing, 400/500
#       {"success": false, "code": -3, "msg": ...} — order_check
#     Плюс ответы шлюза Kong: {"message": "API rate limit exceeded"}.
#     Каждая схема ниже описана отдельно и привязана к тем эндпоинтам, где она
#     реально встречается. Причёсывать это в спецификации нельзя — клиенты
#     разбирают то, что приходит на самом деле.
#   * Успешные ответы тоже разноформатные: конверт {"detail": {code,msg,data}}
#     у заказов, плоский {"status": "success", ...} у userinfo/prices,
#     {"success": true, "data": {...}} у pricing.

openapi: 3.1.0

info:
  title: NETTS API
  version: '2.3'
  summary: |
    Rent TRON energy, run Host Mode, screen addresses for AML and estimate USDT transfer costs.
  description: |
    Programmatic access to the NETTS platform: TRON energy rental, continuous energy supply for an address (Host Mode), AML address screening and a USDT transfer cost calculator.

    ## Quick start

    1. Register in the [dashboard](https://www.netts.io/workspace/) — an API key
    is issued automatically.
    2. Add your server IP to the whitelist (up to 3 addresses) in the API section.
    3. Fund your balance in the Wallet section.
    4. Send the key in the `X-API-KEY` header and the whitelisted IP in `X-Real-IP`.

    ## Response formats

    The API has no single envelope: different endpoints answer in different shapes, and each response schema below reflects that. The general rule is that `code = 10000` or `success: true` means success, while a negative `code` means an error.

    ## Rate limits

    Limits are enforced at the Kong gateway and differ per endpoint — from 1 request per minute (`/prices`) to 50 requests per second (`/order1h`, `/order5m`). The exact value is stated on each operation. Exceeding a limit returns `429` with a body from Kong and a `Retry-After` header. name: NETTS Support email: energy@netts.io url: https://t.me/netts_support
  contact:
    name: NETTS Support
    email: energy@netts.io
    url: https://t.me/netts_support
  license:
    name: Proprietary
    url: https://netts.io

servers:
  - url: https://netts.io/apiv2
    description: Production

security:
  - ApiKeyAuth: []

tags:
  - name: Account
    description: 'Account details and balance.'
  - name: Pricing
    description: 'Current prices for every service.'
  - name: Energy
    description: 'Ordering energy and working with orders.'
  - name: Bandwidth
    description: 'Bandwidth rental. Access is granted by support on request.'
  - name: Host Mode
    description: 'Renting energy to your own address pool and reselling it.'
  - name: AML
    description: 'TRON address screening through external AML providers.'
  - name: Sub-users
    description: 'Managing sub-users (reseller scheme).'
  - name: USDT
    description: 'Estimating the cost of a USDT transfer between two addresses.'
  - name: Webhooks
    description: 'Notifications NETTS sends to your callback URL.'
  - name: Balances
    description: 'Reading TRON balances: current, at a past point, dynamics and period turnover.'
  - name: Reports
    description: 'Statement files built in the background, their status and delivery notifications.'

components:

  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY
      description: |
        API key from the dashboard. Some Host Mode endpoints also accept the key as an `api_key` field in the request body — this is stated in the schema of the particular operation.

  parameters:
    RealIp:
      name: X-Real-IP
      in: header
      required: true
      description: |
        An IP address from the key's whitelist. A request from an address outside the whitelist is rejected with 401.
      schema:
        type: string
        format: ipv4
      example: 203.0.113.10

    XFormat:
      name: X-Format
      in: header
      required: false
      description: |
        Compact plain-text response instead of JSON. The response arrives with `Content-Type: text/plain`.
      schema:
        type: string
        enum: [now, compact, short, short1h, count]
      example: now

  headers:
    RateLimitLimit:
      description: 'Request limit for the current window.'
      schema: { type: integer }
    RateLimitRemaining:
      description: 'Requests left in the current window.'
      schema: { type: integer }
    RateLimitReset:
      description: 'Seconds until the window resets.'
      schema: { type: integer }
    RetryAfter:
      description: 'Seconds to wait before the next request.'
      schema: { type: integer }

  schemas:

    # ── Ошибки ────────────────────────────────────────────────────────────────
    # Форматов несколько, потому что их несколько в реальности. Не объединять.

    ErrorDetailString:
      type: object
      title: 'Error: detail as a string'
      description: 'The shape of authentication errors on `/order1h` and `/order5m`.'
      properties:
        detail:
          type: string
          examples: ['Invalid API key or IP not in whitelist']
      required: [detail]

    ErrorDetailObject:
      type: object
      title: 'Error: detail as an object (FastAPI)'
      description: |
        FastAPI's native shape. Returned when the request is rejected before it reaches application logic — for example on the key check in `/pricing`. detail: type: object properties: code: { type: integer, examples: [-1] } msg: { type: string, examples: ['Invalid API key or IP not in whitelist'] } required: [code, msg]
      properties:
        detail:
          type: object
          properties:
            code: { type: integer, examples: [-1] }
            msg: { type: string, examples: ['Invalid API key or IP not in whitelist'] }
          required: [code, msg]
      required: [detail]

    ErrorCodeMsg:
      type: object
      title: 'Error: flat code/msg'
      description: 'The shape of business errors on the energy ordering endpoints.'
      properties:
        code:
          type: integer
          description: 'A negative or four-digit error code.'
          examples: [1004]
        msg:
          type: string
          examples: ['Insufficient funds. Required: 2.23 TRX, Available: 1.50 TRX']
      required: [code, msg]

    ErrorStatusMessage:
      type: object
      title: 'Error: status/message'
      description: 'The legacy shape used by `/userinfo` and `/prices`.'
      properties:
        status: { type: string, const: error }
        code: { type: integer, examples: [401] }
        message: { type: string, examples: ['Invalid API key or IP not in whitelist'] }
      required: [status, message]

    ErrorSuccessObject:
      type: object
      title: 'Error: success + error{}'
      description: 'The shape of application errors on `/pricing`.'
      properties:
        success: { type: boolean, const: false }
        error:
          type: object
          properties:
            code: { type: integer, examples: [4002] }
            message: { type: string, examples: ['Unknown services: invalid_service'] }
          required: [code, message]
      required: [success, error]

    ErrorSuccessCodeMsg:
      type: object
      title: 'Error: success + code/msg'
      description: 'The shape used by `/order_check`.'
      properties:
        success: { type: boolean, const: false }
        code: { type: integer, examples: [-3] }
        msg: { type: string, examples: ['Order not found'] }
      required: [success]

    KongError:
      type: object
      title: 'Gateway error (Kong)'
      description: |
        A response produced by the gateway BEFORE the request reaches the application. The application itself never returns this shape. message: { type: string, examples: ['API rate limit exceeded'] } request_id: { type: string, examples: ['70257e0476dc396a11875532a45ce037'] }
      properties:
        message: { type: string, examples: ['API rate limit exceeded'] }
        request_id: { type: string, examples: ['70257e0476dc396a11875532a45ce037'] }
      required: [message]

    # ── Общие типы ────────────────────────────────────────────────────────────

    TronAddress:
      type: string
      description: 'TRON address in base58 format (TRC-20).'
      pattern: '^T[1-9A-HJ-NP-Za-km-z]{33}$'
      examples: ['TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE']

    TxHash:
      type: string
      description: 'Transaction hash on the TRON network.'
      examples: ['3636f97dde244fca17cdc0b2cf7fd157a4b0de94d8fbd4c190d5378ba911dfae']

    # ── Аккаунт ───────────────────────────────────────────────────────────────

    UserInfo:
      type: object
      title: 'Account information'
      properties:
        status: { type: string, const: success }
        timestamp:
          type: string
          description: 'Server time (UTC) in `YYYY-MM-DD HH:MM:SS` format.'
          examples: ['2025-09-02 05:56:11']
        user_id: { type: integer, examples: [6] }
        user_info:
          type: object
          properties:
            email: { type: string, format: email }
            name: { type: string }
            deposit_address:
              allOf: [{ $ref: '#/components/schemas/TronAddress' }]
              description: 'TRON address for deposits.'
        stats:
          type: object
          properties:
            balance:
              type: number
              description: |
                Total TRX balance = deposit wallet balance + pending payouts (Robot and sub-users) − pending withdrawals. Payouts are credited twice a day and cannot be used to buy energy until they are credited.
              examples: [28361.15]
            total_delegations: { type: integer, examples: [96756] }
            total_energy_delegated: { type: integer, examples: [21687058023] }
            total_trx_spent: { type: number, examples: [832181.95] }
            total_deposit: { type: number, examples: [0] }
            avg_rate_sun_energy: { type: number, examples: [38.37] }
            save_by_netts_percent: { type: number, examples: [61.63] }
            save_in_dollars: { type: number, examples: [453267.36] }
        network_info:
          type: object
          properties:
            trx_price: { type: number, examples: [0.3391] }
            network_energy_fee: { type: number, examples: [100.0] }
      required: [status, user_id]

    # ── Цены ──────────────────────────────────────────────────────────────────

    PricingTier:
      type: object
      description: |
        Volume-based pricing tier. As of 2026-09-05 `tiers` is `null` for every period — the field is reserved for future volume pricing. min_energy: { type: integer, description: 'Lower bound, inclusive.' } max_energy: type: [integer, 'null'] description: 'Upper bound, inclusive; `null` means no limit.' price: { type: integer, description: 'Price per energy unit in SUN.' } label: { type: string, examples: ['65k'] }
      properties:
        min_energy: { type: integer, description: 'Lower bound, inclusive.' }
        max_energy:
          type: [integer, 'null']
          description: 'Upper bound, inclusive; `null` means no limit.'
        price: { type: integer, description: 'Price per energy unit in SUN.' }
        label: { type: string, examples: ['65k'] }

    PricingPeriod:
      type: object
      description: |
        A time-of-day pricing period. The number of periods, their boundaries and labels are set server-side and do change — iterate the array instead of hardcoding it. id: { type: string, description: 'Period slug.' } label: { type: string, examples: ['Daytime Hours (11:00-21:00 UTC)'] } start: { type: string, description: 'Period start, HH:MM UTC.' } end: { type: string, description: 'Period end, HH:MM UTC.' } is_current: { type: boolean } price: { type: integer, description: 'Price per energy unit in SUN.' } tiers: type: [array, 'null'] items: { $ref: '#/components/schemas/PricingTier' }
      properties:
        id: { type: string, description: 'Period slug.' }
        label: { type: string, examples: ['Daytime Hours (11:00-21:00 UTC)'] }
        start: { type: string, description: 'Period start, HH:MM UTC.' }
        end: { type: string, description: 'Period end, HH:MM UTC.' }
        is_current: { type: boolean }
        price: { type: integer, description: 'Price per energy unit in SUN.' }
        tiers:
          type: [array, 'null']
          items: { $ref: '#/components/schemas/PricingTier' }

    PricingServicePeriodic:
      type: object
      description: '`pricing_type: periodic` — used by the `energy_1h` and `energy_5m` services.'
      properties:
        unit: { type: string, examples: [sun] }
        pricing_type: { type: string, const: periodic }
        description: { type: string }
        cache_ttl: { type: integer, description: 'How often this data refreshes, in seconds.' }
        current_period: { type: string }
        periods:
          type: array
          items: { $ref: '#/components/schemas/PricingPeriod' }
      required: [pricing_type]

    PricingServiceFlatRates:
      type: object
      description: '`pricing_type: flat_rates` — used by the `host` service. Prices are in TRX.'
      properties:
        unit: { type: string, examples: [trx] }
        pricing_type: { type: string, const: flat_rates }
        description: { type: string }
        cache_ttl: { type: integer }
        rates:
          type: object
          properties:
            standard_65k: { type: number }
            standard_131k_initial: { type: number }
            frequent_65k: { type: number }
            frequent_131k: { type: number }
      required: [pricing_type]

    PricingServiceProviderBased:
      type: object
      description: |
        `pricing_type: provider_based` — used by the `aml` service. The set of providers is dynamic: iterate the object instead of hardcoding names. unit: { type: string, examples: [usdt] } pricing_type: { type: string, const: provider_based } description: { type: string } cache_ttl: { type: integer } providers: type: object additionalProperties: type: object properties: price: { type: number, description: 'Check price in USDT.' } price_trx: { type: number, description: 'The same price in TRX at the current rate.' } available: { type: boolean, description: 'Whether the provider has quota available.' }
      properties:
        unit: { type: string, examples: [usdt] }
        pricing_type: { type: string, const: provider_based }
        description: { type: string }
        cache_ttl: { type: integer }
        providers:
          type: object
          additionalProperties:
            type: object
            properties:
              price: { type: number, description: 'Check price in USDT.' }
              price_trx: { type: number, description: 'The same price in TRX at the current rate.' }
              available: { type: boolean, description: 'Whether the provider has quota available.' }
      required: [pricing_type]

    PricingServiceBandwidth:
      type: object
      description: |
        `pricing_type: tiered_by_amount_and_period` — the `bandwidth` service. Returned ONLY when requested explicitly with `?services=bandwidth`. The whole grid is server-driven: the number of time windows, their labels and boundaries, the set of rental periods, the amount tiers and the weekday breakdown all change. Clients must iterate `windows`, `schedule`, `tiers` and `periods` and match by value, never by index. Sub-user markups do NOT apply to bandwidth. unit: { type: string, const: sun_per_unit } pricing_type: { type: string, const: tiered_by_amount_and_period } description: { type: string } cache_ttl: { type: integer } window: { type: string, description: 'Label of the current time-of-day window (UTC).' } current_day_of_week: type: integer description: 'Current ISO weekday: 1 is Monday, 7 is Sunday (UTC).' minimum: 1 maximum: 7 tiers: type: array description: 'Tiers for the CURRENT window and day — a convenience slice.' items: { $ref: '#/components/schemas/BandwidthTier' } windows: type: array description: | Directory of every window. A window may cross midnight, in which case `start` is greater than `end`. items: type: object properties: label: { type: string } start: { type: string, examples: ['01:00'] } end: { type: string, examples: ['09:00'] } schedule: type: array description: | The full grid — one entry per weekday and window pair. Exactly one entry has `is_current` set to true. type: object properties: day_of_week: { type: integer, minimum: 1, maximum: 7 } window: { type: string } period_start: { type: string } period_end: { type: string } is_current: { type: boolean } tiers: type: array items: { $ref: '#/components/schemas/BandwidthTier' } surcharges: type: object description: 'Fixed additions to the client price, in TRX.' properties: small_order_threshold_units: type: integer description: 'Orders BELOW this amount get the small-order surcharge.' small_order_surcharge_trx: type: number description: | Small-order surcharge — compensation for the on-chain delegation and reclaim. trx_send_surcharge_trx: type: number description: 'Surcharge applied when the order is fulfilled by sending TRX.' limits: type: object properties: min_units: { type: integer } max_units: { type: integer }
      properties:
        unit: { type: string, const: sun_per_unit }
        pricing_type: { type: string, const: tiered_by_amount_and_period }
        description: { type: string }
        cache_ttl: { type: integer }
        window: { type: string, description: 'Label of the current time-of-day window (UTC).' }
        current_day_of_week:
          type: integer
          description: 'Current ISO weekday: 1 is Monday, 7 is Sunday (UTC).'
          minimum: 1
          maximum: 7
        tiers:
          type: array
          description: 'Tiers for the CURRENT window and day — a convenience slice.'
          items: { $ref: '#/components/schemas/BandwidthTier' }
        windows:
          type: array
          description: |
            Directory of every window. A window may cross midnight, in which case `start` is greater than `end`.
          items:
            type: object
            properties:
              label: { type: string }
              start: { type: string, examples: ['01:00'] }
              end: { type: string, examples: ['09:00'] }
        schedule:
          type: array
          description: |
            The full grid — one entry per weekday and window pair. Exactly one entry has `is_current` set to true.
          items:
            type: object
            properties:
              day_of_week: { type: integer, minimum: 1, maximum: 7 }
              window: { type: string }
              period_start: { type: string }
              period_end: { type: string }
              is_current: { type: boolean }
              tiers:
                type: array
                items: { $ref: '#/components/schemas/BandwidthTier' }
        surcharges:
          type: object
          description: 'Fixed additions to the client price, in TRX.'
          properties:
            small_order_threshold_units:
              type: integer
              description: 'Orders BELOW this amount get the small-order surcharge.'
            small_order_surcharge_trx:
              type: number
              description: |
                Small-order surcharge — compensation for the on-chain delegation and reclaim.
            trx_send_surcharge_trx:
              type: number
              description: 'Surcharge applied when the order is fulfilled by sending TRX.'
        limits:
          type: object
          properties:
            min_units: { type: integer }
            max_units: { type: integer }
      required: [pricing_type]

    BandwidthTier:
      type: object
      properties:
        amount_min: { type: integer, description: 'Lower bound, inclusive.' }
        amount_max:
          type: [integer, 'null']
          description: 'Upper bound, exclusive; `null` means no limit.'
        periods:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                description: 'Rental period id. The set may grow.'
                examples: ['5m', '1h']
              rental_seconds: { type: integer }
              price: { type: integer, description: 'Price per bandwidth unit in SUN.' }

    PricingResponse:
      type: object
      title: 'Response of /pricing'
      properties:
        success: { type: boolean, const: true }
        version: { type: string, examples: ['2.1'] }
        timestamp: { type: string, format: date-time, description: 'Server time, ISO 8601 UTC.' }
        data:
          type: object
          properties:
            trx_rate_usd: { type: number, examples: [0.3391] }
            units_meta:
              type: object
              description: |
                Machine-readable unit conversion factors. SUN to TRX: `trx = sun / units_meta.sun.multiplier`. type: object properties: base: { type: string } multiplier: { type: integer }
              additionalProperties:
                type: object
                properties:
                  base: { type: string }
                  multiplier: { type: integer }
            services:
              type: object
              description: |
                Map of the requested services. Parse each one by its `pricing_type` field.
              additionalProperties:
                oneOf:
                  - { $ref: '#/components/schemas/PricingServicePeriodic' }
                  - { $ref: '#/components/schemas/PricingServiceFlatRates' }
                  - { $ref: '#/components/schemas/PricingServiceProviderBased' }
                  - { $ref: '#/components/schemas/PricingServiceBandwidth' }
      required: [success, data]

    PricesLegacyResponse:
      type: object
      title: 'Response of /prices (deprecated)'
      properties:
        status: { type: string, const: success }
        current_utc_time: { type: string, examples: ['2025-09-02 06:50:39'] }
        current_utc_hour: { type: integer, minimum: 0, maximum: 23 }
        active_period: { type: string, examples: ['05:00-07:00'] }
        last_update: { type: string }
        host_price: { type: number, description: 'Host Mode price in TRX.' }
        periods:
          type: array
          items:
            type: object
            properties:
              label: { type: string }
              period: { type: string, examples: ['05:00-07:00'] }
              is_active: { type: boolean }
              prices:
                type: object
                description: |
                  Three hardcoded volume categories. In `/pricing` they are replaced by a single period price plus the future `tiers`. less_than_200k: type: object properties: { price_sun: { type: integer } } equal_131k: type: object properties: { price_sun: { type: integer } } more_than_200k: type: object properties: { price_sun: { type: integer } }
                properties:
                  less_than_200k:
                    type: object
                    properties: { price_sun: { type: integer } }
                  equal_131k:
                    type: object
                    properties: { price_sun: { type: integer } }
                  more_than_200k:
                    type: object
                    properties: { price_sun: { type: integer } }

    # ── Заказы энергии ────────────────────────────────────────────────────────

    OrderRequest:
      type: object
      properties:
        amount:
          type: integer
          description: |
            Energy amount. The range differs per endpoint: `/order1h` accepts 61,000…3,000,000, `/order5m` accepts 61,000…650,000.
          examples: [131000]
        receiveAddress:
          allOf: [{ $ref: '#/components/schemas/TronAddress' }]
          description: 'The address that will receive the energy.'
      required: [amount, receiveAddress]

    OrderData:
      type: object
      properties:
        orderId:
          type: string
          description: 'Order id: `1H{request_id}` or `5M{id}`.'
          examples: ['1H70bcc7962a']
        paidTRX:
          type: number
          description: 'Total cost in TRX, including address activation if it was needed.'
          examples: [2.535]
        hash:
          type: [string, 'null']
          description: 'Pool address the energy was delegated from.'
        delegateAddress:
          allOf: [{ $ref: '#/components/schemas/TronAddress' }]
          description: 'Pool address the energy was delegated from.'
        energy:
          type: integer
          description: 'Delegated energy including the buffer (usually +50 units, free of charge).'
          examples: [131050]
        activationHash:
          type: string
          description: 'Present only if the address had to be activated.'

    OrderSuccess:
      type: object
      title: 'Successful order'
      properties:
        detail:
          type: object
          properties:
            code: { type: integer, const: 10000 }
            msg: { type: string, examples: ['Successful, 2.23 TRX deducted'] }
            data: { $ref: '#/components/schemas/OrderData' }
          required: [code, msg, data]
      required: [detail]

    OrderSuccessCached:
      allOf:
        - { $ref: '#/components/schemas/OrderSuccess' }
        - type: object
          properties:
            idempotency:
              type: object
              description: 'Marks a cached response for an order that was already completed.'
              properties:
                status: { type: string, examples: [completed] }
                cached: { type: boolean, const: true }
                original_created_at:
                  type: string
                  description: 'Time the original order was accepted, without a timezone.'

    OrderDuplicateProcessing:
      type: object
      title: 'Duplicate still being processed'
      description: |
        An identical request is already running. Do not repeat it — wait for `retry_after_seconds` and check the state through `/order_check`. success: { type: boolean, const: false } error: { type: string, const: duplicate_request_processing } message: { type: string } idempotency_key: { type: string } retry_after_seconds: { type: integer, examples: [3] }
      properties:
        success: { type: boolean, const: false }
        error: { type: string, const: duplicate_request_processing }
        message: { type: string }
        idempotency_key: { type: string }
        retry_after_seconds: { type: integer, examples: [3] }

    OrderCheckResponse:
      type: object
      title: 'Response of /order_check'
      properties:
        success: { type: boolean, const: true }
        code: { type: integer, const: 10000 }
        order:
          type: object
          properties:
            id: { type: string, examples: ['1Ha1b2c3d4e5'] }
            cost: { type: number, description: 'Total paid by the user, in TRX.' }
            target_address: { $ref: '#/components/schemas/TronAddress' }
            energy: { type: integer }
            duration: { type: string, const: '1 hour' }
            tx_hashes:
              type: array
              items: { $ref: '#/components/schemas/TxHash' }
            timestamp:
              type: string
              description: |
                Arrives WITHOUT a timezone (`2025-12-03T15:21:34.182127`), so it is not RFC 3339 — parse it as server time in UTC.
            rate_sun: { type: number, description: 'Effective rate, SUN per energy unit.' }
            comments: { type: [string, 'null'], examples: ['Includes activation: 1.1 TRX'] }
        activation:
          type: [object, 'null']
          description: 'Filled in if the receiver address had to be activated.'
          properties:
            cost: { type: number }
            tx_hash: { $ref: '#/components/schemas/TxHash' }
        subuser_payout:
          type: [object, 'null']
          description: |
            Present only for a parent user looking at their sub-user's order. The sub-user itself does not see this field.
          properties:
            parent_id: { type: integer }
            gross_cost: { type: number, description: 'What the sub-user paid.' }
            energy_markup: { type: number }
            activation_markup: { type: number }
            total_markup: { type: number }
            net_cost: { type: number, description: 'The parent''s actual cost.' }
            subuser_rate_sun: { type: number }
            parent_rate_sun: { type: number }

    # ── Bandwidth, активация, возврат ─────────────────────────────────────────

    ErrorDetailStatusObject:
      type: object
      title: 'Error: detail with status'
      description: |
        The error shape of the bandwidth, activation and reclaim services. It differs from `ErrorDetailObject` by carrying a `status` field.
      properties:
        detail:
          type: object
          properties:
            code: { type: integer, examples: [5003] }
            status: { type: string, examples: [failed] }
            msg: { type: string }
            data: { type: object }
          required: [code, msg]
      required: [detail]

    BandwidthRequest:
      type: object
      properties:
        amount:
          type: integer
          minimum: 400
          maximum: 5000
          description: |
            Number of bandwidth units. Without accreditation only 400 is allowed — anything else returns `5008`.
          examples: [1500]
        receiveAddress:
          allOf: [{ $ref: '#/components/schemas/TronAddress' }]
          description: 'The address that will receive the bandwidth.'
        period:
          type: string
          enum: ['5m', '1h']
          description: 'Rental duration. The bandwidth is reclaimed automatically when it expires.'
        trx_send:
          type: boolean
          default: false
          description: |
            Guaranteed transaction: if there is no bandwidth in the pool, TRX is sent to the address so the transfer still goes through. Works only with `amount = 400`; ignored otherwise.
        check:
          type: boolean
          default: false
          description: |
            Do not delegate and do not charge if the recipient already has more than 400 bandwidth. In that case the status `enough` is returned.
        test:
          type: boolean
          default: false
          description: |
            Dry run: the full order flow is simulated and the response tells you what would happen and what it would cost. No on-chain action and no charge.
      required: [amount, receiveAddress, period]

    BandwidthResponse:
      type: object
      properties:
        detail:
          type: object
          properties:
            code:
              type: integer
              description: |
                `10000` — delegated or TRX sent, `10001` — accepted by an external provider, `10002` — bandwidth was already sufficient, `10003` — test run, `10004` — reclaimed.
              examples: [10000]
            status:
              type: string
              enum: [completed, enough, processing, test, reclaimed, failed]
            msg: { type: string }
            data:
              type: object
              properties:
                orderId:
                  type: string
                  description: 'Order id: `B5M…` for five minutes, `B1H…` for an hour.'
                paidTRX: { type: number, description: 'TRX charged. With the `enough` status it is 0.' }
                fulfilledBy:
                  type: string
                  enum: [bandwidth, trx]
                  description: |
                    How the order was fulfilled — by delegating bandwidth or by sending TRX.
                hash:
                  type: array
                  description: |
                    Delegation hashes, up to 10 per order. The field is always an array; it is empty when the order was fulfilled by sending TRX.
                  items: { $ref: '#/components/schemas/TxHash' }
                trxSendHash:
                  type: array
                  description: 'TRX transfer hashes. Present only when `fulfilledBy = trx`.'
                  items: { $ref: '#/components/schemas/TxHash' }
                reclaimHash:
                  type: array
                  description: 'Bandwidth reclaim hashes.'
                  items: { $ref: '#/components/schemas/TxHash' }
                bandwidth: { type: integer }
                period: { type: string, enum: ['5m', '1h'] }
                testAction:
                  type: string
                  description: |
                    What would happen on a real order. Only with `test=true`. A value of `would_error:<reason>` carries the reason for the refusal, for example `would_error:no_bandwidth`.
                  examples: [would_delegate, would_trx_send, enough]
                wouldCostTRX: { type: number, description: 'What would be charged. Only with `test=true`.' }
                receiverFreeBandwidth: { type: integer, description: 'The recipient''s free bandwidth at the moment of the check.' }

    ActivatorRequest:
      type: object
      properties:
        targetAddress:
          allOf: [{ $ref: '#/components/schemas/TronAddress' }]
          description: 'The address to activate.'
      required: [targetAddress]

    ActivatorResponse:
      type: object
      properties:
        detail:
          type: object
          properties:
            code:
              type: integer
              description: |
                `10000` — the address was activated and you were charged; `2001` — it was already active and nothing was charged.
            msg: { type: string }
            data:
              type: object
              properties:
                orderId:
                  type: string
                  description: 'Activation order number, always `A` followed by digits.'
                  examples: ['A123456']
                targetAddress: { $ref: '#/components/schemas/TronAddress' }
                activation_cost: { type: number, description: 'TRX charged. Fixed at 1.1; with code 2001 it is 0.' }
                user_balance_before: { type: number }
                user_balance_after: { type: number }
                user_balance: { type: number, description: 'Balance reported with code 2001.' }
                status: { type: string, examples: [already_activated] }
                activation_required: { type: boolean }
                hash: { $ref: '#/components/schemas/TxHash' }

    ReclaimResponse:
      type: object
      properties:
        detail:
          type: object
          properties:
            code: { type: integer, const: 10004 }
            status: { type: string, const: reclaimed }
            msg: { type: string, examples: ['Energy reclaimed', 'Energy already reclaimed'] }
            data:
              type: object
              properties:
                orderId: { type: string }
                reclaimHash:
                  description: |
                    Reclaim transaction hash. For energy this is a string, for bandwidth an array of strings.
                    - { $ref: '#/components/schemas/TxHash' }
                    - type: array
                    items: { $ref: '#/components/schemas/TxHash' }
                  oneOf:
                    - { $ref: '#/components/schemas/TxHash' }
                    - type: array
                      items: { $ref: '#/components/schemas/TxHash' }

    # ── Оркестратор (батч) ────────────────────────────────────────────────────

    OrchestratorItemOptions:
      type: object
      description: |
        Per-address step options. Any field can also be set in `defaults` for the whole order; a value on the address itself wins over the default. bandwidth: type: boolean default: true description: 'Order bandwidth for the address when it is short of it.' bandwidthAmount: type: integer enum: [400, 5000] default: 400 bandwidthPeriod: type: string enum: ['5m', '1h'] default: '1h' check: type: boolean description: | Check free bandwidth first and skip the order if there is enough. The default depends on the amount: `true` when `bandwidthAmount` is 400 and `false` when it is 5000 — ordering five thousand usually means you want them regardless of what the address already has. type: boolean default: false description: 'Passed through to the bandwidth service as is.' activation: type: boolean default: true description: | Activate the address if it is not active. Set `false` for addresses you know are already active.
      properties:
        bandwidth:
          type: boolean
          default: true
          description: 'Order bandwidth for the address when it is short of it.'
        bandwidthAmount:
          type: integer
          enum: [400, 5000]
          default: 400
        bandwidthPeriod:
          type: string
          enum: ['5m', '1h']
          default: '1h'
        check:
          type: boolean
          description: |
            Check free bandwidth first and skip the order if there is enough. The default depends on the amount: `true` when `bandwidthAmount` is 400 and `false` when it is 5000 — ordering five thousand usually means you want them regardless of what the address already has.
        trx_send:
          type: boolean
          default: false
          description: 'Passed through to the bandwidth service as is.'
        activation:
          type: boolean
          default: true
          description: |
            Activate the address if it is not active. Set `false` for addresses you know are already active.

    OrchestratorRequest:
      type: object
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 100
          description: |
            Addresses in the order, from 1 to 100. Duplicates of the same address inside one order are rejected (`5006`).
          items:
            allOf:
              - { $ref: '#/components/schemas/OrchestratorItemOptions' }
              - type: object
                properties:
                  receiveAddress: { $ref: '#/components/schemas/TronAddress' }
                  amount:
                    type: integer
                    minimum: 61000
                    maximum: 50000000
                    description: |
                      Energy for this address. Amounts may differ between addresses; the limit is shared — the sum over all addresses in the order must not exceed 50,000,000.
                required: [receiveAddress, amount]
        clientRequestId:
          type: string
          pattern: '^[A-Za-z0-9._:-]{8,128}$'
          description: |
            Your order reference. If the `X-Idempotency-Key` header is absent, it serves as the idempotency key. With neither, the request is rejected with `5010`.
        defaults:
          allOf: [{ $ref: '#/components/schemas/OrchestratorItemOptions' }]
          description: 'Values applied to every address that does not override them.'
      required: [items]

    OrchestratorAccepted:
      type: object
      description: |
        The order is queued. This is NOT a delivery receipt: nothing has been executed or charged yet — read the result from `statusUrl`. detail: type: object properties: code: { type: integer, const: 10202 } status: { type: string, const: accepted } msg: { type: string } data: type: object properties: idempotencyKey: { type: string } requestId: { type: integer } itemsAccepted: { type: integer } statusUrl: { type: string } originalAcceptedAt: type: string format: date-time description: 'Present in the 208 response — when the original order was accepted.' items: type: array items: type: object properties: deliveryKey: { type: integer } trackingId: type: string description: | The pair of idempotency key and address — the identity of one address inside the order, for your own logs and reconciliation. amount: { type: integer } energyChunks: { type: integer } activation: { type: string } bandwidth: { type: string } status: { type: string }
      properties:
        detail:
          type: object
          properties:
            code: { type: integer, const: 10202 }
            status: { type: string, const: accepted }
            msg: { type: string }
            data:
              type: object
              properties:
                idempotencyKey: { type: string }
                requestId: { type: integer }
                itemsAccepted: { type: integer }
                statusUrl: { type: string }
                originalAcceptedAt:
                  type: string
                  format: date-time
                  description: 'Present in the 208 response — when the original order was accepted.'
                items:
                  type: array
                  items:
                    type: object
                    properties:
                      deliveryKey: { type: integer }
                      trackingId:
                        type: string
                        description: |
                          The pair of idempotency key and address — the identity of one address inside the order, for your own logs and reconciliation.
                      receiveAddress: { $ref: '#/components/schemas/TronAddress' }
                      amount: { type: integer }
                      energyChunks: { type: integer }
                      activation: { type: string }
                      bandwidth: { type: string }
                      status: { type: string }

    OrchestratorStatus:
      type: object
      properties:
        detail:
          type: object
          properties:
            code: { type: integer, const: 10000 }
            status: { type: string, examples: [processing, completed] }
            data:
              type: object
              properties:
                idempotencyKey: { type: string }
                requestId: { type: integer }
                clientRequestId: { type: string }
                summary:
                  type: object
                  description: 'Address counters by status.'
                  additionalProperties: { type: integer }
                items:
                  type: array
                  items:
                    type: object
                    properties:
                      deliveryKey: { type: integer }
                      trackingId: { type: string }
                      receiveAddress: { $ref: '#/components/schemas/TronAddress' }
                      status:
                        type: string
                        enum: [queued, processing, completed, partial, failed, insufficient_balance, credentials_revoked, cancelled]
                        description: |
                          `partial` — some chunks delivered, some not. `insufficient_balance` — the balance fell below the minimum and the address was not attempted. `credentials_revoked` — the API key was disabled while the order was running.
                      energy:
                        type: object
                        properties:
                          requested: { type: integer }
                          delegated: { type: integer }
                          status: { type: string, enum: [done, partial, failed] }
                          chunks:
                            type: object
                            properties:
                              total: { type: integer }
                              done: { type: integer }
                          orderIds:
                            type: array
                            description: |
                              Energy order numbers — pass the same ones to the energy reclaim endpoint.
                            items: { type: string }
                          hashes:
                            type: array
                            description: |
                              Your proof of delivery. With an external provider the hash is unknown at order time and appears about a minute later; an address is not reported as finished until the hashes are collected or the wait window expires.
                            items: { $ref: '#/components/schemas/TxHash' }
                      activation:
                        type: object
                        properties:
                          status:
                            type: string
                            enum: [not_needed, done, failed, skipped, skipped_unavailable]
                          orderId: { type: [string, 'null'] }
                          hash: { type: [string, 'null'] }
                      bandwidth:
                        type: object
                        properties:
                          status: { type: string, enum: [enough, done, failed, skipped] }
                          orderId: { type: [string, 'null'] }
                          amount: { type: integer }
                          period: { type: string }
                          hashes: { type: array, items: { $ref: '#/components/schemas/TxHash' } }
                          skipReason:
                            type: [string, 'null']
                            description: |
                              `option_off` — you disabled the step; `energy_gt_600000` — large energy orders do not need a bandwidth top-up.
                      attempts: { type: integer }
                      startedAt: { type: string, format: date-time }
                      finishedAt: { type: string, format: date-time }

    # ── Вывод TRX ─────────────────────────────────────────────────────────────

    WithdrawRequest:
      type: object
      properties:
        amount:
          type: number
          minimum: 3
          description: |
            Gross amount in TRX, before the fee. The recipient receives `amount − fee`. The order is rejected if nothing positive is left after the fee.
          examples: [15]
        address:
          allOf: [{ $ref: '#/components/schemas/TronAddress' }]
          description: 'Destination address.'
        sub_and_robot_out:
          type: boolean
          default: false
          description: 'Robot and sub-user payout mode — the fee is 2 TRX instead of 1.'
      required: [amount, address]

    WithdrawResponse:
      type: object
      properties:
        detail:
          type: object
          properties:
            code: { type: integer, examples: [10000, 10001] }
            status: { type: string, enum: [pending, completed, failed] }
            msg: { type: string }
            data:
              type: object
              properties:
                orderId:
                  type: string
                  description: |
                    Order number — a 43-character URL-safe string. It equals the idempotency key and is passed to the status endpoint as is, without URL-encoding.
                amount: { type: number, description: 'Gross amount before the fee.' }
                fee: { type: number, description: 'Fee withheld: 1 TRX or 2 TRX.' }
                net: { type: number, description: 'What the recipient receives: amount − fee.' }
                address: { $ref: '#/components/schemas/TronAddress' }
                processed_at: { type: string, description: 'Time of execution; appears once the order is finished.' }
                error_message: { type: [string, 'null'], description: 'Failure reason for the `failed` status.' }

    WithdrawWebhookConfig:
      type: object
      properties:
        callback_url:
          type: string
          format: uri
          maxLength: 2048
          description: 'The URL that will receive the POST.'
        secret:
          type: string
          minLength: 8
          maxLength: 256
          description: 'Turns delivery off without deleting the configuration.'
        enabled:
          type: boolean
          default: true
          description: 'Turns delivery off without deleting the configuration.'
      required: [callback_url, secret]

    WithdrawWebhookPayload:
      type: object
      description: |
        The body NETTS sends to your `callback_url` when a withdrawal reaches a terminal state. orderId: { type: string } status: { type: string, enum: [completed, failed] } amount: { type: number } fee: { type: number } net: { type: number } address: { $ref: '#/components/schemas/TronAddress' } processed_at: { type: string } error_message: { type: [string, 'null'] }
      properties:
        orderId: { type: string }
        status: { type: string, enum: [completed, failed] }
        amount: { type: number }
        fee: { type: number }
        net: { type: number }
        address: { $ref: '#/components/schemas/TronAddress' }
        processed_at: { type: string }
        error_message: { type: [string, 'null'] }

    # ── Host Mode ─────────────────────────────────────────────────────────────
    #
    # ВАЖНО про коды HTTP. Эндпоинты Host Mode отдают свои ошибки с HTTP 200 и
    # отрицательным `code` в теле — проверять надо `code`, а не статус ответа.
    # Тело ошибки всегда содержит "data": null. Часть ошибок при этом приходит
    # ДО входа в эндпоинт (недостаток баланса, заблокированный ключ, провал
    # валидации) — у них нормальные коды 402/403/422 и совсем другая форма тела.

    HostEnvelope:
      type: object
      title: 'Host Mode envelope'
      description: |
        The common envelope of Host Mode responses. `code = 0` means success, a negative code means an error — and it arrives with HTTP 200. code: type: integer description: '`0` means success; a negative value means an error.' msg: { type: string } data: type: [object, 'null'] description: 'Always `null` on errors.'
      properties:
        code:
          type: integer
          description: '`0` means success; a negative value means an error.'
        msg: { type: string }
        data:
          type: [object, 'null']
          description: 'Always `null` on errors.'
      required: [code, msg]

    HostValidationError:
      type: object
      title: 'Host Mode validation error (422)'
      description: |
        FastAPI's native shape. There is no `code` field here, unlike every other error of these endpoints. detail: type: array items: { type: object }
      properties:
        detail:
          type: array
          items: { type: object }
      required: [detail]

    HostAddressStatus:
      type: object
      description: 'State of a single address in Host Mode.'
      properties:
        address: { $ref: '#/components/schemas/TronAddress' }
        status: { type: string, enum: [active, inactive] }
        mode:
          type: string
          enum: [normal, infinity, 'off']
          description: '`off` — when the address is inactive.'
        cycles_ordered:
          type: [integer, 'null']
          description: |
            Ordered cycles across open orders. In infinity mode this is `null`, which means unlimited — NOT that the cycles have run out.
        cycle_set: { type: integer }
        cycles_completed:
          type: integer
          description: '`cycles_ordered − cycles_completed`. In infinity mode it is `null`.'
        cycles_remaining:
          type: [integer, 'null']
          description: '`cycles_ordered − cycles_completed`. In infinity mode it is `null`.'
        cycles_total_lifetime:
          type: integer
          description: 'Every delegation for this address over its whole history. Never reset.'
        current_cycle_no:
          type: [integer, 'null']
          description: |
            Position of the current delegation since the address was last started. `null` when no delegation is active.
        open_orders: { type: integer }
        total_spent: { type: number, description: 'Actually charged for this address, in TRX.' }
        bandwidth_delegated:
          type: boolean
          description: |
            Whether bandwidth is currently delegated. It is free and already included in the cycle price.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    AmlErrorEnvelope:
      type: object
      title: 'AML error'
      description: |
        Note that the body carries both `message` and `msg`: validation errors (`4001`, `4002`) use `msg`, everything else uses `message`. This is not a typo in the specification but the behaviour of the service. success: { type: boolean, const: false } error: type: object properties: code: { type: integer, examples: [4020] } message: { type: string } msg: { type: string } required: [code] timestamp: { type: string }
      properties:
        success: { type: boolean, const: false }
        error:
          type: object
          properties:
            code: { type: integer, examples: [4020] }
            message: { type: string }
            msg: { type: string }
          required: [code]
        timestamp: { type: string }
      required: [success, error]

    AmlCheckRequest:
      type: object
      properties:
        address:
          type: string
          minLength: 10
          maxLength: 100
          description: 'The address to screen.'
        network:
          type: string
          description: |
            Network ticker. Required. Elliptic performs holistic screening: the check runs for a specific address on a specific network, but all associated assets are traced — tokens, cross-chain transfers and interactions with known entities on other networks.
          enum: [algo, apt, arb, avax, base, bch, bnb, bsc, btc, celo, cro, ada, atom, doge, dot, dydx, etc, eth, fil, flr, ftm, gnosis, hbar, hype, icp, inj, linea, ltc, matic, mob, near, okb, op, sei, sol, strk, sui, tao, ton, trx, xdc, xlm, xrp, xtz, zec, zen, zil, zksync]
          examples: [trx]
        provider:
          type: string
          enum: [elliptic]
          default: elliptic
        wait:
          type: boolean
          default: false
          description: '`rate` — the risk score only, `full` — complete data.'
        response_format:
          type: string
          enum: [rate, full]
          default: full
          description: '`rate` — the risk score only, `full` — complete data.'
        report_language:
          type: string
          enum: [en]
          default: en
      required: [address, network]

    AmlCheckResponse:
      type: object
      properties:
        success: { type: boolean, const: true }
        data:
          type: object
          properties:
            client_order_id:
              type: string
              description: 'Check identifier — use it to fetch the result.'
              examples: ['A4C666ABE24BD4A']
            status:
              type: string
              enum: [pending, processing, completed, failed]
            address: { type: string }
            provider: { type: string, enum: [elliptic] }
            report_language: { type: string }
            price_usdt: { type: number }
            price_trx: { type: number }
            currency: { type: string, examples: [TRX] }
            message: { type: string }
            risk_score:
              type: [number, 'null']
              description: |
                Elliptic risk score on a 0…10 scale: 0 means no risk, 10 the maximum. `null` means no triggers were detected.
            risk_level: { type: [string, 'null'], enum: [low, medium, high, null] }
            is_sanctioned:
              type: [boolean, 'null']
              description: 'Whether exposure to sanctioned entities was detected.'
            created_at: { type: string }
            completed_at: { type: [string, 'null'] }
            result:
              type: object
              description: |
                The provider's full report. Its structure is defined by Elliptic and is deliberately NOT spelled out field by field here: it is a deeply nested document (source and destination contributions, cluster entities, matched rules, values in USD and in the native asset) that changes on the provider's side. Pinning it down in the schema would guarantee drift from reality. The key values are lifted to the top — `risk_score`, `risk_level`, `is_sanctioned`. A full report example is on the documentation page.
              additionalProperties: true
        timestamp: { type: string }

    AmlHistoryResponse:
      type: object
      properties:
        success: { type: boolean, const: true }
        data:
          type: object
          properties:
            address: { type: string }
            total: { type: integer, description: 'Total number of checks for this address.' }
            page: { type: integer }
            pages: { type: integer, description: 'Number of the last available page.' }
            checks:
              type: array
              description: 'Up to 100 records per page, newest first.'
              items:
                type: object
                properties:
                  client_order_id: { type: string }
                  provider: { type: string }
                  status: { type: string, enum: [completed, pending, processing, failed] }
                  created_at: { type: string }
                  completed_at: { type: [string, 'null'] }
                  risk_score: { type: [number, 'null'] }
                  risk_level: { type: [string, 'null'] }
                  is_sanctioned: { type: [boolean, 'null'] }
        timestamp: { type: string }

    # ── Субпользователи, USDT-калькулятор, ротация адреса ─────────────────────

    SubUser:
      type: object
      properties:
        user_id: { type: integer }
        google_id:
          type: string
          description: |
            **Not a real Google identifier.** Format `SUB` plus 18 digits, used only for internal identification of the sub-user.
          examples: ['SUB123456789012345678']
        email:
          type: string
          description: '**Not a real email address** — it is `google_id` plus `@gmail.com`.'
        name: { type: string, description: 'Display name, defaults to `google_id`. Can be changed in the dashboard.' }
        deposit_address:
          allOf: [{ $ref: '#/components/schemas/TronAddress' }]
          description: 'Generated deposit address. NOT activated by default.'
        api_key: { type: string, description: 'The sub-user''s API key, starting with the `mock` prefix.' }
        referral_code: { type: string, maxLength: 6 }
        created_at:
          type: string
          description: 'Creation time, without a timezone.'
        referrer_user_id: { type: integer, description: 'Parent account id.' }
        comment: { type: string }

    TransferAnalysis:
      type: object
      title: 'USDT transfer cost analysis'
      description: |
        Computed in real time from the current NETTS resource prices and the on-chain state of both addresses, so consecutive calls may differ slightly.
      properties:
        sender: { $ref: '#/components/schemas/AddressInfo' }
        receiver: { $ref: '#/components/schemas/AddressInfo' }
        requirements:
          type: object
          properties:
            energy_needed: { type: integer, description: 'Raw energy the transfer requires.' }
            bandwidth_needed: { type: integer }
            energy_with_buffer: { type: integer, description: 'Energy rounded up to a safe rental tier, for example 131,000.' }
            bandwidth_with_buffer: { type: integer }
            receiver_has_usdt: { type: boolean, description: 'Whether the receiver already holds USDT — this affects energy consumption.' }
        costs:
          type: object
          properties:
            energy_burn_trx: { type: number, description: 'TRX burned if energy is paid for by burning.' }
            bandwidth_burn_trx: { type: number }
            total_burn_trx: { type: number }
            total_burn_sun: { type: integer }
            energy_rental_trx: { type: number, description: 'Cost of renting the required energy from NETTS.' }
            energy_rental_sun: { type: integer }
            rental_time_period:
              type: string
              description: '`1h`, `5m`, or `not_needed` when renting is not the cheaper path.'
            rental_price_per_unit: { type: integer, description: 'Rental price per energy unit in SUN.' }
            savings_trx: { type: number, description: 'How much cheaper renting is than burning. A negative value means burning wins.' }
            savings_percentage: { type: number }
            recommended_method: { type: string, enum: [burn, rent] }
            total_cost_trx: { type: [number, 'null'], description: 'The actual cost if you follow the recommendation.' }
            sender_activation_cost: { type: [number, 'null'], description: 'Extra cost if the sender address needs activation.' }
        can_transfer: { type: boolean }
        issues:
          type: array
          description: 'Concerns detected during the analysis.'
          items: { type: string }
        recommendations:
          type: array
          items: { type: string }
        variation_id: { type: [string, 'null'], examples: ['CUSTOM'] }

    AddressInfo:
      type: object
      description: |
        On-chain state of the address. Integrations normally need `address`, `is_activated`, `trx_balance`, `usdt_balance`, `has_usdt`, `energy_balance` and `bandwidth_balance`; the remaining fields are low-level. address: { $ref: '#/components/schemas/TronAddress' } is_activated: { type: boolean } trx_balance: { type: number } usdt_balance: { type: number } has_usdt: { type: boolean } energy_balance: { type: integer } bandwidth_balance: { type: integer } trx_balance_sun: { type: integer } energy_total: { type: integer } bandwidth_total: { type: integer } bandwidth_free: { type: integer } bandwidth_staked: { type: integer } energy_used: { type: integer } bandwidth_used: { type: integer } create_time: { type: integer } latest_operation_time: { type: integer } staked_for_energy: { type: number } staked_for_bandwidth: { type: number } delegated_for_energy: { type: number } delegated_for_bandwidth: { type: number } delegated_out_energy: { type: number } delegated_out_bandwidth: { type: number } votes: { type: integer }
      properties:
        address: { $ref: '#/components/schemas/TronAddress' }
        is_activated: { type: boolean }
        trx_balance: { type: number }
        usdt_balance: { type: number }
        has_usdt: { type: boolean }
        energy_balance: { type: integer }
        bandwidth_balance: { type: integer }
        trx_balance_sun: { type: integer }
        energy_total: { type: integer }
        bandwidth_total: { type: integer }
        bandwidth_free: { type: integer }
        bandwidth_staked: { type: integer }
        energy_used: { type: integer }
        bandwidth_used: { type: integer }
        create_time: { type: integer }
        latest_operation_time: { type: integer }
        staked_for_energy: { type: number }
        staked_for_bandwidth: { type: number }
        delegated_for_energy: { type: number }
        delegated_for_bandwidth: { type: number }
        delegated_out_energy: { type: number }
        delegated_out_bandwidth: { type: number }
        votes: { type: integer }

    TransferAnalysisResponse:
      type: object
      properties:
        status: { type: string, const: success }
        data: { $ref: '#/components/schemas/TransferAnalysis' }
        current_utc_time: { type: string }
        processing_time_ms: { type: number }

    AddressRotation:
      type: object
      properties:
        rotation_id: { type: integer }
        user_id: { type: integer }
        address_type: { type: string, enum: [web, tg] }
        old_address: { $ref: '#/components/schemas/TronAddress' }
        new_address:
          allOf: [{ $ref: '#/components/schemas/TronAddress' }]
          description: 'Appears once the rotation is complete.'
        status: { type: string, enum: [pending, processing, completed, failed] }
        message: { type: string }
        estimated_completion: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time }

    # ── Вебхуки заказов ───────────────────────────────────────────────────────

    WebhookEndpoint:
      type: object
      properties:
        id: { type: integer }
        url: { type: string, format: uri }
        role:
          type: string
          enum: [primary, backup]
          description: |
            `primary` — the address every notification is delivered to. `backup` — the fallback, used only when delivery to the primary has exhausted its retries.
        is_active: { type: boolean }
        secret:
          type: string
          description: |
            Signing secret. Returned ONLY on creation and on rotation. It never appears in the list or when reading a single endpoint.
          examples: ['whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx']
        created_at:
          type: string
          description: 'Creation time, without a timezone.'
        updated_at:
          type: string
          description: 'Last update time, without a timezone.'

    WebhookEnvelope:
      type: object
      description: 'Envelope of webhook management responses carrying a single endpoint in `data`.'
      properties:
        detail:
          type: object
          properties:
            code: { type: integer, examples: [10000] }
            status: { type: string, examples: [created, ok, updated] }
            data: { $ref: '#/components/schemas/WebhookEndpoint' }

    WebhookListEnvelope:
      type: object
      description: 'Response to a request for the list of delivery endpoints.'
      properties:
        detail:
          type: object
          properties:
            code: { type: integer, examples: [10000] }
            status: { type: string, examples: [ok] }
            data:
              type: object
              properties:
                endpoints:
                  type: array
                  items: { $ref: '#/components/schemas/WebhookEndpoint' }
                count: { type: integer }
                max_endpoints: { type: integer, const: 2 }
                roles:
                  type: array
                  items: { type: string, enum: [primary, backup] }

    WebhookSecretEnvelope:
      type: object
      description: 'Response to a secret rotation — the secret is shown once.'
      properties:
        detail:
          type: object
          properties:
            code: { type: integer, examples: [10000] }
            status: { type: string, examples: [rotated] }
            data:
              type: object
              properties:
                id: { type: integer }
                secret: { type: string }

    WebhookEventCommon:
      type: object
      description: 'Fields common to every event.'
      properties:
        event:
          type: string
          enum: [delegation.confirmed, bandwidth.delegated, activation.confirmed]
          description: |
            Delivery id — the deduplication key on your side. Also sent in the `X-Netts-Delivery` header. A message that first failed on the primary and then succeeded on the backup keeps the same `delivery_id` and remains ONE event.
        delivery_id:
          type: integer
          description: |
            Delivery id — the deduplication key on your side. Also sent in the `X-Netts-Delivery` header. A message that first failed on the primary and then succeeded on the backup keeps the same `delivery_id` and remains ONE event.
        order_id: { type: string }
        order_type: { type: string, enum: ['1h', '5m', bandwidth, activation] }
        tx_hashes:
          type: array
          description: |
            All hashes of the operation. Each one is verified on-chain before sending: a notification either carries hashes that are already in a block, or is not sent at all.
          items: { $ref: '#/components/schemas/TxHash' }
        confirmed_at: { type: string, format: date-time }
      required: [event, delivery_id, order_id, order_type, tx_hashes, confirmed_at]

    DelegationConfirmed:
      allOf:
        - { $ref: '#/components/schemas/WebhookEventCommon' }
        - type: object
          properties:
            receive_address: { $ref: '#/components/schemas/TronAddress' }
            energy_amount: { type: integer }
            tx_hash:
              type: string
              description: 'Optional field, not always present.'
            delegation_timestamp:
              type: integer
              description: 'Optional field, not always present.'

    BandwidthDelegated:
      allOf:
        - { $ref: '#/components/schemas/WebhookEventCommon' }
        - type: object
          properties:
            rental_label: { type: string, examples: ['1h'] }
            rental_seconds: { type: integer, examples: [3600] }
            receive_address: { $ref: '#/components/schemas/TronAddress' }
            bandwidth_amount: { type: integer }
            fulfillment:
              type: string
              enum: [delegated, trx_send, already_enough]
              description: |
                How the order was fulfilled: `delegated` — bandwidth delegated from the pool, `trx_send` — TRX sent instead of delegating, `already_enough` — the address already had enough free bandwidth. The last case is the only one where `tx_hashes` is empty: the order closed successfully, but no transaction was needed.

    ActivationConfirmed:
      allOf:
        - { $ref: '#/components/schemas/WebhookEventCommon' }
        - type: object
          properties:
            address: { $ref: '#/components/schemas/TronAddress' }
            activation_type:
              type: string
              enum: [ACC_CREATE, DIRECT]
              description: '`ACC_CREATE` — AccountCreateContract, `DIRECT` — a TRX transfer.'
            source:
              type: string
              description: |
                Origin marker: either a service tag, or the number of the energy order that required the activation.

    BalanceEnvelope:
      type: object
      description: 'Standard NETTS envelope. Payload is in `data`.'
      properties:
        status: { type: string, enum: [success], examples: [success] }
        code: { type: integer, examples: [0] }
        msg: { type: string, examples: [''] }

    BalanceRow:
      type: object
      description: 'One token held by the address.'
      properties:
        token_id: { type: string, description: '`TRX`, a TRC20 contract address, or a numeric TRC10 id.', examples: [TRX] }
        symbol: { type: string, examples: [TRON] }
        token_type: { type: string, enum: [TRX, TRC10, TRC20], examples: [TRX] }
        decimals: { type: [integer, 'null'], examples: [6] }
        balance: { type: string, description: 'Amount at the requested point. Decimal as a string so precision is never lost to a float.', examples: ['1141.899000'] }
        price_usd: { type: [string, 'null'], description: 'Price of one token. `null` when the token is not in the verified list.', examples: ['0.334968'] }
        value_usd: { type: [string, 'null'], description: 'Position value. `null` when there is no price.', examples: ['382.50'] }
        is_verified: { type: boolean, description: 'Token is in the verified list.', examples: [true] }
        is_spam: { type: boolean, description: 'Token is classified as deceptive.', examples: [false] }
        node_balance: { type: [string, 'null'], description: 'Balance read from a TRON node at request time — always the present value, even in a historical reply, so never use it as the historical amount. For TRX it is the full formula: liquid balance plus staked frozenV2 plus what is delegated out. It is `null` for most TRC20 tokens by design, because a node cannot enumerate the TRC20 holdings of an address, and also `null` when the node is unreachable; TRX and TRC10 always carry a value.', examples: ['1141.899000'] }

    BalanceList:
      allOf:
        - $ref: '#/components/schemas/BalanceEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                address: { type: string, examples: [TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX] }
                as_of_block: { type: [integer, 'null'], description: 'Block the `balance` values stand on. This is the ledger watermark, several minutes behind the chain head, not the newest block.', examples: [86012345] }
                as_of_time: { type: [string, 'null'], description: 'Moment the reply reflects, UTC.', examples: ['2026-09-01 12:00:00 UTC'] }
                total_value_usd: { type: [string, 'null'], examples: ['385.25'] }
                live: { type: boolean, description: 'Whether the live tail was applied, echoing the `live` query parameter.' }
                hide_spam: { type: boolean, description: 'Whether deceptive tokens were hidden.' }
                balances:
                  type: array
                  items: { $ref: '#/components/schemas/BalanceRow' }

    ErrorEnvelope:
      type: object
      description: 'Error in the standard NETTS envelope.'
      properties:
        status: { type: string, enum: [error], examples: [error] }
        code: { type: integer, examples: [-4] }
        msg: { type: string, examples: ['block or ts is required'] }
        data: { type: object, additionalProperties: true }

    BalanceHistoryPoint:
      type: object
      properties:
        date: { type: string, description: 'Day in UTC.', examples: ['2026-09-01'] }
        balance: { type: string, description: 'Balance at the end of that day.', examples: ['1141.899000'] }
        delta: { type: [string, 'null'], description: 'Change during the day. Negative means outflow.', examples: ['-75352.041406'] }
        price_usd: { type: [string, 'null'], examples: ['0.322386'] }
        balance_usd: { type: [string, 'null'], description: 'Balance valued at that day price.', examples: ['368.13'] }

    BalanceHistory:
      allOf:
        - $ref: '#/components/schemas/BalanceEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                address: { type: string, examples: [TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX] }
                token_id: { type: string, examples: [TRX] }
                days: { type: [integer, 'null'], examples: [90] }
                opening_balance: { type: string, examples: ['148067.807102'] }
                has_usd: { type: boolean, description: 'Whether a price was known for this token in the period.' }
                points:
                  type: array
                  items: { $ref: '#/components/schemas/BalanceHistoryPoint' }

    BalanceSummaryToken:
      type: object
      properties:
        token_id: { type: string, examples: [TRX] }
        symbol: { type: string, examples: [TRON] }
        token_type: { type: string, enum: [TRX, TRC10, TRC20] }
        opening_balance: { type: string, examples: ['76493.940406'] }
        period_in: { type: string, description: 'Everything received in the period.', examples: ['141490.950160'] }
        period_out: { type: string, description: 'Everything sent in the period.', examples: ['216763.731366'] }
        period_fees: { type: string, description: 'Fees paid in the period.', examples: ['79.260200'] }
        closing_balance: { type: string, examples: ['1141.899000'] }
        value_usd: { type: [string, 'null'], examples: ['382.50'] }

    BalanceSummary:
      allOf:
        - $ref: '#/components/schemas/BalanceEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                address: { type: string, examples: [TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX] }
                period:
                  type: object
                  properties:
                    from: { type: string, format: date }
                    to: { type: string, format: date }
                tokens:
                  type: array
                  items: { $ref: '#/components/schemas/BalanceSummaryToken' }

    StatementOperation:
      type: object
      properties:
        ts: { type: string, description: 'Moment of the operation, UTC.', examples: ['2026-09-01 00:20:06'] }
        date: { type: string, format: date }
        tx: { type: string, description: 'Transaction hash.' }
        type: { type: string, description: 'Direction of the operation.', examples: [OUT] }
        description: { type: string, examples: ['TRX transfer out'] }
        counterparty: { type: string, description: 'The other address.' }
        counterparty_name: { type: string, description: 'Known label of the counterparty, empty when unknown.' }
        in: { type: string, description: 'Amount received. `0` for an outgoing operation.', examples: ['0.000000'] }
        out: { type: string, description: 'Amount sent. `0` for an incoming operation.', examples: ['1500.000000'] }
        fee: { type: string, examples: ['0.345000'] }
        balance_after: { type: string, description: 'Balance right after the operation.' }
        is_spam: { type: boolean }

    StatementPreview:
      allOf:
        - $ref: '#/components/schemas/BalanceEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                address: { type: string, examples: [TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX] }
                token_id: { type: string, examples: [TRX] }
                period:
                  type: object
                  properties:
                    from: { type: string, format: date }
                    to: { type: string, format: date }
                opening_balance: { type: string }
                closing_balance: { type: string }
                net_change: { type: string }
                totals:
                  type: object
                  description: 'Sums over the period.'
                  properties:
                    in: { type: string }
                    out: { type: string }
                    fee: { type: string }
                reconciliation:
                  type: object
                  description: 'Self-check by the engine. `status` other than `OK` means the numbers did not add up and should not be relied on.'
                  properties:
                    status: { type: string, examples: [OK] }
                    formula: { type: string }
                operations_total: { type: integer, description: 'Operations in the period in full.', examples: [197] }
                operations_truncated: { type: boolean, description: 'Whether `operations` is shorter than `operations_total`.' }
                operations:
                  type: array
                  items: { $ref: '#/components/schemas/StatementOperation' }

    ReportOrder:
      allOf:
        - $ref: '#/components/schemas/BalanceEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                order_no: { type: string, description: 'Order number. Use it for status and download.', examples: [REPxxxxxxxxxxxx] }
                client_request_id: { type: string }
                type: { type: string, enum: [statement, balance_at_date] }
                status: { type: string, enum: [queued, running, processing, done, error, expired] }
                address: { type: string }
                token_id: { type: [string, 'null'] }
                date_from: { type: [string, 'null'], format: date }
                date_to: { type: [string, 'null'], format: date }
                format: { type: [string, 'null'], enum: [csv, pdf, 'null'] }
                progress_pct: { type: integer, examples: [100] }
                created_at: { type: string, description: 'UTC.' }
                finished_at: { type: [string, 'null'], description: 'UTC.' }
                expires_at: { type: [string, 'null'], description: 'UTC. Files are kept for 30 days.' }
                download_url: { type: [string, 'null'], description: 'Present only while the status is `done`.' }
                artifact_sha256: { type: [string, 'null'] }
                artifact_size: { type: [integer, 'null'] }
                error_code: { type: [string, 'null'] }
                error_message: { type: [string, 'null'] }

    ReportWebhook:
      allOf:
        - $ref: '#/components/schemas/BalanceEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                id: { type: integer }
                url: { type: string }
                role: { type: string, enum: [primary, backup] }
                is_active: { type: boolean }
                created_at: { type: string, description: 'UTC.' }
                updated_at: { type: string, description: 'UTC.' }

    ReportWebhookCreated:
      allOf:
        - $ref: '#/components/schemas/BalanceEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                id: { type: integer }
                url: { type: string }
                role: { type: string, enum: [primary, backup] }
                is_active: { type: boolean }
                created_at: { type: string }
                updated_at: { type: string }
                secret: { type: string, description: 'Signing secret. Returned only on creation and rotation.', examples: ['whsec_...'] }

    ReportWebhookList:
      allOf:
        - $ref: '#/components/schemas/BalanceEnvelope'
        - type: object
          properties:
            data:
              type: array
              items:
                type: object
                properties:
                  id: { type: integer }
                  url: { type: string }
                  role: { type: string, enum: [primary, backup] }
                  is_active: { type: boolean }
                  created_at: { type: string }
                  updated_at: { type: string }

  responses:

    RateLimited:
      description: |
        Rate limit exceeded. The response comes from the Kong gateway. Wait for the time given in the `Retry-After` header.
      headers:
        Retry-After: { $ref: '#/components/headers/RetryAfter' }
        RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
        RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
        RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/KongError' }
          example:
            message: API rate limit exceeded

    GatewayTimeout:
      description: 'The gateway did not receive a response from the application in time.'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/KongError' }
          example:
            message: An invalid response was received from the upstream server

    Unauthorized:
      description: 'The API key is missing or invalid, or the source IP is not in the key whitelist.'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
          example: { status: error, code: -2, msg: 'Invalid API key or IP not in whitelist', data: {} }

    BalanceRequired:
      description: 'The account balance is too low. Top up to keep using the API.'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
          example: { status: error, code: 1004, msg: 'Insufficient funds. Please top up your account.', data: {} }

    ValidationError:
      description: 'A parameter is missing or malformed.'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
          example: { status: error, code: -4, msg: 'on: field required', data: { errors: [ { field: 'on', msg: 'field required' } ] } }

    EngineUnavailable:
      description: 'The balance engine did not answer. The request was not counted against any limit; retry.'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
          example: { status: error, code: -4, msg: 'balance engine unavailable', data: {} }

paths:

  /userinfo:
    get:
      operationId: getUserInfo
      summary: 'Account information and balance'
      description: |
        Returns user details, balance and accumulated statistics. The user is identified by the API key; the request takes no parameters.
      tags: [Account]
      parameters:
        - $ref: '#/components/parameters/RealIp'
      responses:
        '200':
          description: 'Account data.'
          headers:
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UserInfo' }
              example:
                status: success
                timestamp: '2025-09-02 05:56:11'
                user_id: 6
                user_info:
                  email: user@example.com
                  name: User Name
                  deposit_address: TWd4hMwsxAB2xbdqrbmDtVv3kfWqCkuvxs
                stats:
                  balance: 28361.15
                  total_delegations: 96756
                  total_energy_delegated: 21687058023
                  total_trx_spent: 832181.95
                  total_deposit: 0
                  avg_rate_sun_energy: 38.37
                  save_by_netts_percent: 61.63
                  save_in_dollars: 453267.36
                network_info:
                  trx_price: 0.3391
                  network_energy_fee: 100.0
        '401':
          description: 'The key is missing or invalid, or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorStatusMessage' }
              example:
                status: error
                code: 401
                message: Invalid API key or IP not in whitelist
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: 'Internal server error.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorStatusMessage' }
              example:
                status: error
                message: Internal server error
      x-rate-limit: 6 requests per minute per IP

  /pricing:
    get:
      operationId: getPricing
      summary: 'Prices for every service'
      description: |
        The recommended pricing endpoint. Returns the prices of all services in one response, with dynamic time-of-day periods.

        Replaces the deprecated `/prices`.

        **The price may change while an order is being fulfilled.** A provider may decline a delegation, in which case NETTS routes the order to the next available one — so an order may be fulfilled above the quoted price. This applies only to orders of 300,000 energy units and above.

        Periods, providers and rates are dynamic: iterate the arrays from the response instead of hardcoding their number, labels and ids. Parse each service by its `pricing_type` field.

        Sub-users receive prices with their parent's markup already applied — no client-side calculation is needed.
      tags: [Pricing]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - $ref: '#/components/parameters/XFormat'
        - name: services
          in: query
          required: false
          description: |
            Comma-separated service filter. By default every service is returned except `bandwidth`, which must be requested explicitly. type: string
          schema:
            type: string
          examples:
            energyOnly:
              summary: 'Energy for one hour only'
              value: energy_1h
            energyAndAml:
              summary: 'Energy and AML'
              value: energy_1h,aml
            bandwidth:
              summary: 'Bandwidth (explicit request only)'
              value: bandwidth
      responses:
        '200':
          description: |
            Prices. With `X-Format` set, a `text/plain` string arrives instead of JSON, carrying the price of the currently active `energy_1h` period. application/json: schema: { $ref: '#/components/schemas/PricingResponse' } example: success: true version: '2.1' timestamp: '2026-09-05T09:00:00Z' data: trx_rate_usd: 0.3391 units_meta: sun: { base: trx, multiplier: 1000000 } trx: { base: trx, multiplier: 1 } usdt: { base: usdt, multiplier: 1 } services: energy_1h: unit: sun pricing_type: periodic description: 1-hour energy delegation cache_ttl: 30 current_period: daytime periods:
                        - id: daytime
            label: Daytime Hours (11:00-21:00 UTC) start: '11:00' end: '21:00' is_current: true price: 27 tiers: null text/plain: schema: { type: string } examples: now: summary: 'X-Format: now' value: 'Daytime Hours (11:00-21:00 UTC): price=27 sun, 65k=1.755 TRX (0.52$), 131k=3.537 TRX (1.05$), 1m=27.000 TRX (7.99$)' short1h: summary: 'X-Format: short1h' value: '65k=1.755 TRX (0.52$), 131k=3.537 TRX (1.05$), 1m=27.000 TRX (7.99$)' count: summary: 'X-Format: count — cost of 1, 2, 3, 5, 10 and 20 orders' value: '1-1.690 TRX (0.50$), 2-3.380 TRX (1.00$), 3-5.070 TRX (1.50$), 5-8.450 TRX (2.50$), 10-16.900 TRX (5.00$), 20-33.800 TRX (10.00$)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PricingResponse' }
              example:
                success: true
                version: '2.1'
                timestamp: '2026-09-05T09:00:00Z'
                data:
                  trx_rate_usd: 0.3391
                  units_meta:
                    sun: { base: trx, multiplier: 1000000 }
                    trx: { base: trx, multiplier: 1 }
                    usdt: { base: usdt, multiplier: 1 }
                  services:
                    energy_1h:
                      unit: sun
                      pricing_type: periodic
                      description: 1-hour energy delegation
                      cache_ttl: 30
                      current_period: daytime
                      periods:
                        - id: daytime
                          label: Daytime Hours (11:00-21:00 UTC)
                          start: '11:00'
                          end: '21:00'
                          is_current: true
                          price: 27
                          tiers: null
            text/plain:
              schema: { type: string }
              examples:
                now:
                  summary: 'X-Format: now'
                  value: 'Daytime Hours (11:00-21:00 UTC): price=27 sun, 65k=1.755 TRX (0.52$), 131k=3.537 TRX (1.05$), 1m=27.000 TRX (7.99$)'
                short1h:
                  summary: 'X-Format: short1h'
                  value: '65k=1.755 TRX (0.52$), 131k=3.537 TRX (1.05$), 1m=27.000 TRX (7.99$)'
                count:
                  summary: 'X-Format: count — cost of 1, 2, 3, 5, 10 and 20 orders'
                  value: '1-1.690 TRX (0.50$), 2-3.380 TRX (1.00$), 3-5.070 TRX (1.50$), 5-8.450 TRX (2.50$), 10-16.900 TRX (5.00$), 20-33.800 TRX (10.00$)'
        '400':
          description: 'The `services` parameter names a service that does not exist.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorSuccessObject' }
              example:
                success: false
                error:
                  code: 4002
                  message: 'Unknown services: invalid_service'
        '401':
          description: |
            The key is missing or invalid, or the IP is not whitelisted. The shape differs from the other errors of this endpoint: the error is raised before the request reaches application logic and is returned in FastAPI's native format. application/json: schema: { $ref: '#/components/schemas/ErrorDetailObject' } example: detail: { code: -1, msg: Invalid API key or IP not in whitelist }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }
              example:
                detail: { code: -1, msg: Invalid API key or IP not in whitelist }
        '404':
          description: 'No user found for this key.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }
              example:
                detail: { code: -1, msg: User not found }
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: |
            Failed to retrieve pricing data (`5001`) or to build the compact format (`5002`). application/json: schema: { $ref: '#/components/schemas/ErrorSuccessObject' } example: success: false error: { code: 5001, message: Failed to retrieve pricing data }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorSuccessObject' }
              example:
                success: false
                error: { code: 5001, message: Failed to retrieve pricing data }
        '504':
          $ref: '#/components/responses/GatewayTimeout'

  /prices:
    get:
      operationId: getPricesLegacy
      summary: 'Energy prices (deprecated)'
      deprecated: true
      description: |
        The deprecated pricing endpoint, scheduled for removal. Move to `/pricing`: it has dynamic periods, a service filter, `energy_5m`, AML and host prices in one response, and machine-readable units.

        Returns five fixed time-of-day periods and three hardcoded volume categories.

        The rate limit here is the strictest in the whole API — one request per minute, because the response is assembled by heavy database queries.
      tags: [Pricing]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - $ref: '#/components/parameters/XFormat'
      responses:
        '200':
          description: 'Prices by time-of-day period.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PricesLegacyResponse' }
            text/plain:
              schema: { type: string }
              example: 'Daytime Hours (11:00-21:00 UTC): price=27 sun, 65k=1.755 TRX (0.52$), 131k=3.537 TRX (1.05$), 1m=27.000 TRX (7.99$)'
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorStatusMessage' }
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: 'Internal server error.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorStatusMessage' }
      x-rate-limit: 1 request per minute and 60 per hour per IP

  /order1h:
    post:
      operationId: createOrder1h
      summary: 'Order energy for 1 hour'
      description: |
        Hourly energy rental. The order goes through several providers with automatic failover: the cheapest available one is picked, and if it declines the order moves to the next.

        Energy normally arrives in 0.5–10 seconds. The response timeout is 10 seconds. If the receiver address is not activated on TRON, NETTS activates it at cost price and the response may take up to 6 seconds.

        A buffer of +50 energy units is added to the order automatically, free of charge, as compensation to the provider.

        **Idempotency.** Request uniqueness is determined by the combination of the timestamp (a 1-second window), the amount, the receiver address and the key. Repeating the same request does not charge you twice: you get `208` with the cached response, or `409` if the first one is still running. Identical requests must not be sent in parallel.
      tags: [Energy]
      parameters:
        - $ref: '#/components/parameters/RealIp'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - { $ref: '#/components/schemas/OrderRequest' }
                - type: object
                  properties:
                    amount:
                      type: integer
                      minimum: 61000
                      maximum: 3000000
            example:
              amount: 131000
              receiveAddress: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
      responses:
        '200':
          description: 'The order was fulfilled and the energy delegated.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrderSuccess' }
              example:
                detail:
                  code: 10000
                  msg: Successful, 2.23 TRX deducted
                  data:
                    orderId: '1H123456'
                    paidTRX: 2.23
                    hash: a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345678
                    delegateAddress: TNp5gsJhBmZFXgCdgjMgr8pEZ8fHgXUHDq
                    energy: 131050
        '208':
          description: |
            A duplicate of an order that was already processed — the cached response is returned. Nothing was charged a second time. application/json: schema: { $ref: '#/components/schemas/OrderSuccessCached' } example: detail: code: 10000 msg: Successful, 2.54 TRX deducted data: hash: 9e4c20e21e01e4c39b21b670d1ea1fc1e4b0de94d8fbd4c190d5378ba911dfae energy: 65050 orderId: 1H70bcc7962a paidTRX: 2.535 delegateAddress: TNp5gsJhBmZFXgCdgjMgr8pEZ8fHgXUHDq idempotency: status: completed cached: true original_created_at: '2025-12-03T10:34:49.104896'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrderSuccessCached' }
              example:
                detail:
                  code: 10000
                  msg: Successful, 2.54 TRX deducted
                  data:
                    hash: 9e4c20e21e01e4c39b21b670d1ea1fc1e4b0de94d8fbd4c190d5378ba911dfae
                    energy: 65050
                    orderId: 1H70bcc7962a
                    paidTRX: 2.535
                    delegateAddress: TNp5gsJhBmZFXgCdgjMgr8pEZ8fHgXUHDq
                idempotency:
                  status: completed
                  cached: true
                  original_created_at: '2025-12-03T10:34:49.104896'
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }
              example:
                detail: Invalid API key or IP not in whitelist
        '403':
          description: 'Insufficient balance (code `1004`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorCodeMsg' }
              example:
                code: 1004
                msg: 'Insufficient funds. Required: 2.23 TRX, Available: 1.50 TRX'
        '409':
          description: 'An identical request is being processed right now. Do not retry.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrderDuplicateProcessing' }
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: 'Internal server error (code `5000`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorCodeMsg' }
              example: { code: 5000, msg: Internal server error occurred }
        '503':
          description: |
            Energy providers unavailable. Codes: `5001` and `5002` — a provider is temporarily unavailable, `5003` — all of them are, `5004` — the provider requires a higher minimum amount. application/json: schema: { $ref: '#/components/schemas/ErrorCodeMsg' } example: code: 5003 msg: Service temporarily unavailable. All energy providers are currently unavailable.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorCodeMsg' }
              example:
                code: 5003
                msg: Service temporarily unavailable. All energy providers are currently unavailable.
      x-rate-limit: 50 requests per second per IP

  /order5m:
    post:
      operationId: createOrder5m
      summary: 'Order energy for 5 minutes'
      description: |
        Five-minute energy rental from the internal NETTS pools. External providers are not used here, unlike `/order1h`.

        Because of that, a temporary refusal is possible under peak load: on a `503` wait 2–3 seconds and retry, or fall back to `/order1h`, which has external providers.

        The price is lower than the hourly rate. An inactive receiver address is activated automatically and charged once per address; the activation hash arrives in a separate `activationHash` field.
      tags: [Energy]
      parameters:
        - $ref: '#/components/parameters/RealIp'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - { $ref: '#/components/schemas/OrderRequest' }
                - type: object
                  properties:
                    amount:
                      type: integer
                      minimum: 61000
                      maximum: 650000
            example:
              amount: 65000
              receiveAddress: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
      responses:
        '200':
          description: 'The order was fulfilled.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrderSuccess' }
              examples:
                plain:
                  summary: 'Plain order'
                  value:
                    detail:
                      code: 10000
                      msg: Successful, 1.430 TRX deducted
                      data:
                        orderId: 5Mb4ee11ef86
                        paidTRX: 1.43
                        hash: 3636f97dde244fca17cdc0b2cf7fd157a4b0de94d8fbd4c190d5378ba911dfae
                        delegateAddress: TNp5gsJhBmZFXgCdgjMgr8pEZ8fHgXUHDq
                        energy: 65050
                withActivation:
                  summary: 'With receiver address activation'
                  value:
                    detail:
                      code: 10000
                      msg: Successful, 1.430 TRX for energy + 1.100 TRX for address activation
                      data:
                        orderId: 5Mb4ee11ef86
                        paidTRX: 2.53
                        hash: 3636f97dde244fca17cdc0b2cf7fd157a4b0de94d8fbd4c190d5378ba911dfae
                        delegateAddress: TNp5gsJhBmZFXgCdgjMgr8pEZ8fHgXUHDq
                        energy: 65050
                        activationHash: bab38070a64b237acc9110ecf5135acc0de94d8fbd4c190d5378ba911dfae12
        '208':
          description: |
            A duplicate of an already processed order — cached response, nothing charged twice.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrderSuccessCached' }
        '400':
          description: |
            The amount is outside 61,000…650,000 (code `1003`), or the user has no payer address configured (code `1005`). application/json: schema: { $ref: '#/components/schemas/ErrorCodeMsg' } example: code: 1003 msg: 'Energy amount must be between 61000 and 650000. Requested: 50000'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorCodeMsg' }
              example:
                code: 1003
                msg: 'Energy amount must be between 61000 and 650000. Requested: 50000'
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }
        '403':
          description: 'Insufficient balance (code `1004`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorCodeMsg' }
        '409':
          description: |
            An identical request is being processed right now. The uniqueness window is 2 seconds.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrderDuplicateProcessing' }
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: 'Internal server error (code `5000`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorCodeMsg' }
        '503':
          description: |
            The internal pools are temporarily at capacity (code `5003`). This is a normal response, not a failure: retry in 2–3 seconds or fall back to `/order1h`. application/json: schema: { $ref: '#/components/schemas/ErrorCodeMsg' } example: code: 5003 msg: Service temporarily unavailable. Energy delegation failed after retries.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorCodeMsg' }
              example:
                code: 5003
                msg: Service temporarily unavailable. Energy delegation failed after retries.
      x-rate-limit: 50 requests per second per IP

  /order_check:
    get:
      operationId: checkOrder
      summary: 'Details of a 1-hour order'
      description: |
        Details of an `/order1h` order: cost, receiver address, transaction hashes, rate, address activation and the sub-user markup breakdown.

        The main use is fetching the delegation hash when `/order1h` returned an empty `hash` field. Query it no earlier than a minute after the order.

        You only see your own orders. A parent user sees the orders of their sub-users and additionally receives a `subuser_payout` object with the markup breakdown; the sub-user itself does not see that field.

        Only 1-hour orders created through `/order1h` are returned.
      tags: [Energy]
      parameters:
        - name: order_id
          in: query
          required: true
          description: 'Order id in `1H{hash}` format.'
          schema:
            type: string
            pattern: '^1H[0-9a-fA-F]+$'
          example: 1Ha1b2c3d4e5
      responses:
        '200':
          description: 'Order data.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrderCheckResponse' }
              examples:
                plain:
                  summary: 'Plain order'
                  value:
                    success: true
                    code: 10000
                    order:
                      id: 1Ha1b2c3d4e5
                      cost: 2.535
                      target_address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
                      energy: 65000
                      duration: 1 hour
                      tx_hashes: [a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345678]
                      timestamp: '2025-12-03T15:21:34.182127'
                      rate_sun: 39.0
                      comments: null
                    activation: null
                    subuser_payout: null
                withSubuserPayout:
                  summary: 'A sub-user''s order as the parent sees it'
                  value:
                    success: true
                    code: 10000
                    order:
                      id: 1Ha1b2c3d4e5
                      cost: 3.93
                      target_address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
                      energy: 65000
                      duration: 1 hour
                      tx_hashes: [c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456789abc]
                      timestamp: '2025-12-03T16:55:16.525118'
                      rate_sun: 60.5
                      comments: 'Includes activation: 1.1 TRX'
                    activation:
                      cost: 1.1
                      tx_hash: b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456789a
                    subuser_payout:
                      parent_id: 12
                      gross_cost: 3.93
                      energy_markup: 0.195
                      activation_markup: 0.1
                      total_markup: 0.295
                      net_cost: 3.635
                      subuser_rate_sun: 43.5
                      parent_rate_sun: 39.0
        '400':
          description: 'The `order_id` parameter is missing (code `-2`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorSuccessCodeMsg' }
              example: { success: false, code: -2, msg: order_id parameter required }
        '401':
          description: 'The key is missing or invalid (code `-1`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorSuccessCodeMsg' }
              example: { success: false, code: -1, msg: API key not provided }
        '403':
          description: 'The order belongs to another user (code `-4`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorSuccessCodeMsg' }
              example: { success: false, code: -4, msg: Order does not belong to this user }
        '404':
          description: 'Order not found (code `-3`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorSuccessCodeMsg' }
              example: { success: false, code: -3, msg: Order not found }
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: 'Internal server error.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorSuccessCodeMsg' }
      x-rate-limit: 50 requests per second per IP

  /bandwidth:
    post:
      operationId: createBandwidthOrder
      summary: 'Rent bandwidth'
      description: |
        Bandwidth rental on TRON, delegated to a recipient address for five minutes or an hour. The bandwidth is reclaimed automatically when the period ends.

        **Access is tiered.** Accredited accounts rent any amount within the pool and the limits, with several concurrent orders. Without accreditation you may rent 400 units, one order at a time: the next is possible only after the previous rental ends. A request for any other amount is rejected with `5008`, a second concurrent order with `5007`. Accreditation is granted by NETTS support.

        There is no buffer here: exactly the requested amount is delegated. One order may produce up to ten delegation hashes — the `hash` field is always an array.

        Fixed surcharges are added to the price: 0.372 TRX for an order below 1000 units (compensation for the on-chain delegation and reclaim), or 0.268 TRX if the order is fulfilled by sending TRX.

        **Idempotency** works through the `X-Idempotency-Key` header. Only successful results are cached (`completed` and `enough`): if the previous attempt failed or timed out and nothing was charged, the same key can safely be resent — the order is attempted again.
      tags: [Bandwidth]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: X-Idempotency-Key
          in: header
          required: false
          description: |
            Idempotency key, base64, 16–64 characters from `A–Z a–z 0–9 + / = _ -`. Built as `base64(HMAC-SHA256(api_key, "receiveAddress:amount:period:nonce"))`, where `nonce` is stable across retries of one order and differs between distinct orders. The period must be part of the string: renting the same address for 5m and for 1h are different orders. If the header is omitted, the server derives the key itself from the request parameters within a short time window. A malformed or over-long key is rejected with 400 (`5004`). type: string pattern: '^[A-Za-z0-9+/=_-]{16,64}$'
          schema:
            type: string
            pattern: '^[A-Za-z0-9+/=_-]{16,64}$'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BandwidthRequest' }
            example:
              amount: 1500
              receiveAddress: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
              period: 5m
      responses:
        '200':
          description: |
            The order was processed. Read `detail.code`: `10000` — bandwidth delegated or TRX sent, `10002` — the recipient already had enough bandwidth and nothing was charged, `10003` — a test run. application/json: schema: { $ref: '#/components/schemas/BandwidthResponse' } examples: delegated: summary: 'Bandwidth delegated' value: detail: code: 10000 status: completed msg: Successful data: orderId: B5Ma1b2c3d4e5f6 paidTRX: 1.24 fulfilledBy: bandwidth hash: [a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456] bandwidth: 1500 period: 5m trxSent: summary: 'No bandwidth available — TRX sent' value: detail: code: 10000 status: completed msg: Successful (sent TRX, bandwidth unavailable) data: orderId: B5Ma1b2c3d4e5f6 paidTRX: 0.268 fulfilledBy: trx trxSendHash: [d4e5f6789012345678901234567890abcdef1234567890abcdef123456789012] hash: [] bandwidth: 400 period: 5m enough: summary: 'The recipient already has enough (check=true)' value: detail: code: 10002 status: enough msg: enough band for 1 transfer data: { orderId: B5Ma1b2c3d4e5f6, paidTRX: 0, bandwidth: 400, period: 5m } test: summary: 'Test run (test=true)' value: detail: code: 10003 status: test msg: Test run — no on-chain action, no charge data: orderId: B5Ma1b2c3d4e5f6 testAction: would_delegate wouldCostTRX: 0.772 paidTRX: 0 bandwidth: 400 period: 5m receiverFreeBandwidth: 600
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BandwidthResponse' }
              examples:
                delegated:
                  summary: 'Bandwidth delegated'
                  value:
                    detail:
                      code: 10000
                      status: completed
                      msg: Successful
                      data:
                        orderId: B5Ma1b2c3d4e5f6
                        paidTRX: 1.24
                        fulfilledBy: bandwidth
                        hash: [a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456]
                        bandwidth: 1500
                        period: 5m
                trxSent:
                  summary: 'No bandwidth available — TRX sent'
                  value:
                    detail:
                      code: 10000
                      status: completed
                      msg: Successful (sent TRX, bandwidth unavailable)
                      data:
                        orderId: B5Ma1b2c3d4e5f6
                        paidTRX: 0.268
                        fulfilledBy: trx
                        trxSendHash: [d4e5f6789012345678901234567890abcdef1234567890abcdef123456789012]
                        hash: []
                        bandwidth: 400
                        period: 5m
                enough:
                  summary: 'The recipient already has enough (check=true)'
                  value:
                    detail:
                      code: 10002
                      status: enough
                      msg: enough band for 1 transfer
                      data: { orderId: B5Ma1b2c3d4e5f6, paidTRX: 0, bandwidth: 400, period: 5m }
                test:
                  summary: 'Test run (test=true)'
                  value:
                    detail:
                      code: 10003
                      status: test
                      msg: Test run — no on-chain action, no charge
                      data:
                        orderId: B5Ma1b2c3d4e5f6
                        testAction: would_delegate
                        wouldCostTRX: 0.772
                        paidTRX: 0
                        bandwidth: 400
                        period: 5m
                        receiverFreeBandwidth: 600
        '202':
          description: |
            The order was handed to an external provider and runs asynchronously. Poll `/bandwidth/status/{orderId}` until it completes. application/json: schema: { $ref: '#/components/schemas/BandwidthResponse' } example: detail: code: 10001 status: processing msg: Order accepted, processed by an external provider. Poll the status endpoint. data: { orderId: B5Ma1b2c3d4e5f6, bandwidth: 1500, period: 5m }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BandwidthResponse' }
              example:
                detail:
                  code: 10001
                  status: processing
                  msg: Order accepted, processed by an external provider. Poll the status endpoint.
                  data: { orderId: B5Ma1b2c3d4e5f6, bandwidth: 1500, period: 5m }
        '208':
          description: |
            A duplicate of an order already fulfilled — cached response, nothing charged twice.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BandwidthResponse' }
        '400':
          description: |
            The amount or period is out of range, or the idempotency key is malformed (`5004`); the user has no payer address configured (`1005`). application/json: schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' } example: detail: { code: 5004, status: failed, msg: Bandwidth amount out of range (400..5000) }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
              example:
                detail: { code: 5004, status: failed, msg: Bandwidth amount out of range (400..5000) }
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
              example:
                detail: { code: -1, msg: Invalid API key or IP not in whitelist }
        '403':
          description: 'Insufficient balance (`1004`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
              example:
                detail: { code: 1004, status: failed, msg: Insufficient funds }
        '409':
          description: 'An identical request is being processed. Wait, do not retry.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrderDuplicateProcessing' }
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: 'Internal server error (`5000`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '503':
          description: |
            Delegation failed or the service is unavailable (`5003`); without accreditation — the previous rental is still active (`5007`) or an amount above 400 was requested (`5008`). application/json: schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' } example: detail: { code: 5003, status: failed, msg: Bandwidth delegation failed }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
              example:
                detail: { code: 5003, status: failed, msg: Bandwidth delegation failed }
      x-rate-limit: 50 requests per second per IP

  /bandwidth/status/{orderId}:
    get:
      operationId: getBandwidthOrderStatus
      summary: 'Bandwidth order status'
      description: |
        State of the order. Needed above all after a `202` response, when the order went to an external provider. The order must belong to the key's owner.
      tags: [Bandwidth]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: orderId
          in: path
          required: true
          description: 'Order id: `B5M…` or `B1H…`.'
          schema: { type: string }
      responses:
        '200':
          description: |
            The order state is in `detail.code`: `10000` — completed, `10001` — in progress, `10002` — bandwidth was already sufficient, `5003` — failed. application/json: schema: { $ref: '#/components/schemas/BandwidthResponse' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BandwidthResponse' }
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '404':
          description: 'The order was not found or belongs to another account.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
              example:
                detail: { code: -1, msg: Order not found }
        '429':
          $ref: '#/components/responses/RateLimited'

  /bandwidth/reclaim/{orderId}:
    post:
      operationId: reclaimBandwidth
      summary: 'Reclaim bandwidth early'
      description: |
        Voluntary return of delegated bandwidth before the period ends. The rental charge is NOT refunded — reclaiming only frees the pool resource ahead of time.

        Calling it twice is safe: the same hash is returned.
      tags: [Bandwidth]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: orderId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: 'Bandwidth reclaimed, or already reclaimed earlier (`10004`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReclaimResponse' }
              example:
                detail:
                  code: 10004
                  status: reclaimed
                  msg: Bandwidth reclaimed
                  data:
                    orderId: B5Ma1b2c3d4e5f6
                    reclaimHash: [51fa77da06e8fbebf504fbf088d1d9611059398d1234567890abcdef12345678]
        '400':
          description: 'Nothing to reclaim — the order is not in a delegated state (`5005`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '404':
          description: 'The order was not found or belongs to another account.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          description: 'The reclaim is not confirmed on-chain yet (`5003`) — retry in a few seconds.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }

  /activator:
    post:
      operationId: activateAddress
      summary: 'Activate a TRON address'
      description: |
        Creates the account record for an address that has never been used on-chain. Until an address is activated, energy and bandwidth cannot be delegated to it.

        The operation is one-time per address and costs a fixed 1.1 TRX. You are charged at the moment of a successful activation; a failed attempt costs nothing. If the address is already active, code `2001` is returned and nothing is charged — check `detail.code` before assuming money was spent.

        The 1-hour energy order activates an inactive address on its own and includes the activation in the order cost. Call this endpoint explicitly when the address must become active BEFORE a rental — for example before ordering bandwidth, which requires an active account.

        The address becomes usable only once the transaction is included in a block, up to ~3 seconds after the response. If your next step depends on the address being active, confirm its state on-chain by the returned hash instead of calling the next method immediately.

        **There is no idempotency.** Repeating the call for an already active address is safe and free, but two simultaneous requests for the same new address may both proceed — serialise activation on your side.

        The legacy path `/apiv2/activation` is served by the same service and behaves identically.
      tags: [Energy]
      parameters:
        - $ref: '#/components/parameters/RealIp'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ActivatorRequest' }
            example:
              targetAddress: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
      responses:
        '200':
          description: |
            The address was activated (`10000`, 1.1 TRX charged) or was already active (`2001`, nothing charged).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ActivatorResponse' }
              examples:
                activated:
                  summary: 'Activated'
                  value:
                    detail:
                      code: 10000
                      msg: Address activated successfully, 1.1 TRX deducted
                      data:
                        orderId: A123456
                        targetAddress: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
                        activation_cost: 1.1
                        user_balance_before: 500.0
                        user_balance_after: 498.9
                        hash: '0000000000000000000000000000000000000000000000000000000000000000'
                alreadyActive:
                  summary: 'Was already active — nothing charged'
                  value:
                    detail:
                      code: 2001
                      msg: Address is already activated
                      data:
                        targetAddress: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
                        status: already_activated
                        user_balance: 500.0
                        activation_cost: 0
                        activation_required: false
        '401':
          description: 'The key is invalid or the IP is not whitelisted (`-1`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '403':
          description: 'Insufficient balance (`1004`). No order is created.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '422':
          description: 'Malformed `targetAddress`: 34 characters, starting with `T`.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: 'Internal server error (`5000`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '503':
          description: |
            Activation failed or the service is temporarily unavailable (`5003`). Nothing was charged and no address was activated — the request may be retried. application/json: schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
      x-rate-limit: 5 requests per second and 60 per minute per IP

  /energy/reclaim/{orderId}:
    post:
      operationId: reclaimEnergy
      summary: 'Reclaim energy early'
      description: |
        Returns delegated energy to the pool before the rental period ends. The operation is voluntary: rentals expire on their own, you never have to call this method.

        Nothing is refunded. Renting and returning are separate operations: you paid when you placed the order, and returning the resource early does not undo it.

        Reclaim works **per order, not per address**. One address may hold energy from several orders, including other accounts'. A batch over many addresses produces one order number per address, and a large amount for a single address may be split across several orders — to free that address completely, reclaim each of them.

        An order can only be reclaimed with the key that created it: another key of the same account will not work.

        Calling it twice is safe — the same hash is returned; this also covers rentals that already expired on their own.
      tags: [Energy]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: orderId
          in: path
          required: true
          description: 'Order number: `1H…` for hourly rentals, `5M…` for five-minute ones.'
          schema: { type: string }
          example: 1Hb252feadcc
      responses:
        '200':
          description: 'Energy reclaimed, or already reclaimed earlier (`10004`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReclaimResponse' }
              example:
                detail:
                  code: 10004
                  status: reclaimed
                  msg: Energy reclaimed
                  data:
                    orderId: 1Hb252feadcc
                    reclaimHash: 51fa77da06e8fbebf504fbf088d1d9611059398d1234567890abcdef12345678
        '400':
          description: |
            Nothing to reclaim: the order produced no delegation, or it is not active (`5005`). Or the order was served by an external provider — those resources cannot be returned (`5006`). application/json: schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' } example: detail: code: 5006 status: failed msg: Reclaim is available only for internal pool orders data: { orderId: 1H47eac983af }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
              example:
                detail:
                  code: 5006
                  status: failed
                  msg: Reclaim is available only for internal pool orders
                  data: { orderId: 1H47eac983af }
        '401':
          description: 'The key is missing or invalid, or the IP is not whitelisted (`-1`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '404':
          description: 'The order was not found or belongs to another account (`-1`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
              example:
                detail: { code: -1, msg: Order not found }
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          description: |
            The reclaim was accepted, but the transaction did not appear on-chain within the wait window (`5003`). It will most likely complete on its own — repeat the call in a few seconds to get the hash. application/json: schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
      x-rate-limit: 10 requests per second per IP

  /orchestrator:
    post:
      operationId: createOrchestratorOrder
      summary: 'Batch order for up to 100 addresses'
      description: |
        One request instead of three per address. For every address the whole sequence runs: probe → activation if the address is not active → a bandwidth top-up if free bandwidth is below 400 → energy rental, with large amounts split into chunks automatically.

        The `202` response arrives immediately, the connection is not held. A failure in activation or bandwidth does not cancel the energy order for that address, and one address failing never affects the others.

        Addresses run in parallel, up to five at a time within one order, so a large batch does not queue behind a single slow address. The order of completion is not guaranteed.

        Amounts above 1,000,000 are split into even chunks automatically, each becoming its own energy order — all their numbers and hashes arrive in `energy.orderIds` and `energy.hashes`.

        **Limits:** 100 addresses per order, 61,000…50,000,000 energy per address, no more than 50,000,000 in total per order, 3 orders and 300 addresses in flight per account, and a minimum balance of 4 TRX to be accepted.

        **Idempotency.** The order is identified by your key — the `X-Idempotency-Key` header or the `clientRequestId` field. A repeat with the same key and the same body returns `208` with the original order; a repeat with the same key and a different body is rejected as `4090 IDEMPOTENCY_CONFLICT`. Inside the order every address carries its own internal key, so a repeat never double-charges an individual address either.

        **Billing.** The orchestrator itself charges nothing: each step is billed by the service that performs it — activation as a separate deduction with an `A…` number, bandwidth with a `B1H…` number and only when actually delegated, energy as one deduction per chunk with `1H…` numbers. If your balance runs out mid-batch, the remaining addresses end as `insufficient_balance` without being attempted.
      tags: [Energy]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: X-Idempotency-Key
          in: header
          required: false
          description: |
            Order key, 12–128 characters from `A-Z a-z 0-9 . _ : -`. Required when the body carries no `clientRequestId`. type: string pattern: '^[A-Za-z0-9._:-]{12,128}$'
          schema:
            type: string
            pattern: '^[A-Za-z0-9._:-]{12,128}$'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/OrchestratorRequest' }
            example:
              clientRequestId: my-batch-2026-01-01-001
              defaults:
                bandwidth: true
                bandwidthAmount: 400
                bandwidthPeriod: 1h
                check: true
                trx_send: false
              items:
                - { receiveAddress: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE, amount: 65000 }
                - { receiveAddress: TNp5gsJhBmZFXgCdgjMgr8pEZ8fHgXUHDq, amount: 131000 }
                - { receiveAddress: TVj7RNVHy6thbM7BWdSe9G6gXwKhjhdNZS, amount: 61000, bandwidth: false }
      responses:
        '202':
          description: 'The order is queued. Nothing has been executed or charged yet.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrchestratorAccepted' }
        '208':
          description: |
            A repeat with the same key and the same body — the original order, no second one was created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrchestratorAccepted' }
        '400':
          description: |
            Bad request. Codes: `5004` — invalid address, amount out of range, `bandwidthAmount` not 400 or 5000, `bandwidthPeriod` not `5m`/`1h`, body not an object; `5005` — `items` empty or missing; `5006` — duplicate address inside one order; `5009` — malformed key; `5010` — no key given, neither in the header nor in the body; `5012` — total energy above 50,000,000. application/json: schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '402':
          description: 'Balance below the 4 TRX minimum (`1004`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '409':
          description: 'The same key with a different body — `4090 IDEMPOTENCY_CONFLICT`.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '422':
          description: 'The request failed validation (`4220`); details are in `data.errors`.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '429':
          description: |
            Rate limit exceeded, or too many orders, addresses or chunks in flight (`5011`). application/json: schema: { $ref: '#/components/schemas/KongError' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/KongError' }
        '503':
          description: |
            The order was NOT accepted — the service is temporarily unavailable (`5003`). Nothing was stored and nothing was charged, so the request can be safely retried. application/json: schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
      x-rate-limit: 20 requests per second per IP

  /orchestrator/status/{idempotencyKey}:
    get:
      operationId: getOrchestratorStatus
      summary: 'Batch progress'
      tags: [Energy]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: idempotencyKey
          in: path
          required: true
          schema: { type: string }
        - name: address
          in: query
          required: false
          description: 'Return data for a single address instead of the whole order.'
          schema: { $ref: '#/components/schemas/TronAddress' }
      responses:
        '200':
          description: 'State of the order and of every address in it.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrchestratorStatus' }
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '404':
          description: 'No order with this key was found, or it belongs to another account.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '429':
          $ref: '#/components/responses/RateLimited'

  /orchestrator/cancel/{idempotencyKey}:
    post:
      operationId: cancelOrchestratorOrder
      summary: 'Cancel the addresses of a batch that have not started'
      description: |
        Removes from the queue every address that has not been picked up yet.

        Addresses in `processing` are NOT interrupted: part of their energy may already be paid for. Cancel is best-effort on the remainder.
      tags: [Energy]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: idempotencyKey
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: 'How many addresses were removed from the queue (`10005`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
              example:
                detail:
                  code: 10005
                  status: cancelled
                  msg: 'Order cancelled: 7 addresses removed from queue'
                  data: { cancelled: 7 }
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '404':
          description: 'The order was not found or belongs to another account.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '429':
          $ref: '#/components/responses/RateLimited'

  /withdraw:
    post:
      operationId: createWithdrawal
      summary: 'Withdraw TRX from your balance'
      description: |
        Withdraws TRX to any TRON address. The order number is returned immediately, while the payout itself is performed by a background process, usually within five minutes. There is never a synchronous on-chain result in the first response — a `pending` acknowledgement always comes first.

        The amount is reserved from your balance the moment the order is accepted, not when the TRX is finally sent.

        The fee is withheld from the amount: 1 TRX normally and 2 TRX with `sub_and_robot_out`. The recipient receives `net = amount − fee`. The minimum is 3 TRX.

        Only one withdrawal at a time may be in progress on your own balance — otherwise `4090`.

        Collect the result by polling the status endpoint or through a webhook.

        **Sub-users withdraw in exactly the same way** — same method, their own key, their own balance. There is no separate endpoint for them.

        **Idempotency.** The `X-Idempotency-Key` BECOMES the order number. Only accepted orders are cached: if the previous attempt failed on insufficient funds or validation, the same key can be resent — the request is processed again.
      tags: [Account]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: X-Idempotency-Key
          in: header
          required: false
          description: |
            Idempotency key, 16–64 characters from `A–Z a–z 0–9 + / = _ -`. Built as `base64url(HMAC-SHA256(api_key, "address:amount:nonce"))` with padding removed. This same key becomes the order's `orderId`. If omitted, the server derives it from the request parameters within a short time window. type: string pattern: '^[A-Za-z0-9+/=_-]{16,64}$'
          schema:
            type: string
            pattern: '^[A-Za-z0-9+/=_-]{16,64}$'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WithdrawRequest' }
            example:
              amount: 15
              address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
      responses:
        '202':
          description: 'The request was accepted, the amount is reserved and the payout is queued.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WithdrawResponse' }
              example:
                detail:
                  code: 10000
                  status: pending
                  msg: Withdrawal request accepted, processing within 5 minutes.
                  data:
                    orderId: EXAMPLEorderId0000000000000000000000000000Aa
                    amount: 15.0
                    fee: 1.0
                    net: 14.0
                    address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
        '208':
          description: |
            A repeat of an already accepted request — cached response, no second withdrawal was created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WithdrawResponse' }
        '400':
          description: |
            Validation error (`5004`): the amount is below 3 TRX, the fee consumes the whole amount, the address is invalid, or the idempotency key is malformed. application/json: schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' } example: detail: { code: 5004, status: failed, msg: Minimum withdrawal is 3 TRX }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
              example:
                detail: { code: 5004, status: failed, msg: Minimum withdrawal is 3 TRX }
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '403':
          description: 'Insufficient balance (`1004`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '409':
          description: |
            Either a withdrawal is already in progress (`4090`), or an identical request is running right now — in both cases there is nothing to retry. application/json: schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' } example: detail: { code: 4090, status: failed, msg: You have a pending withdrawal. Wait until it is processed. }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
              example:
                detail: { code: 4090, status: failed, msg: You have a pending withdrawal. Wait until it is processed. }
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: 'Internal server error (`5000`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '503':
          description: 'The withdrawal service is temporarily unavailable (`5003`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
      x-rate-limit: 5 requests per second and 150 per minute per API key

  /withdraw/status/{orderId}:
    get:
      operationId: getWithdrawalStatus
      summary: 'Withdrawal status'
      description: |
        `orderId` is URL-safe and is passed as is, no encoding needed.

        Read the state from `detail.code`: `10000` — the TRX was sent, `10001` — queued or sending, `5003` — failed, with the reason in `error_message`.
      tags: [Account]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: orderId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: 'State of the request.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WithdrawResponse' }
              example:
                detail:
                  code: 10000
                  status: completed
                  data:
                    orderId: EXAMPLEorderId0000000000000000000000000000Aa
                    amount: 15.0
                    fee: 1.0
                    net: 14.0
                    address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
                    processed_at: '2026-01-01 00:00:00+00:00'
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '404':
          description: 'The request was not found or belongs to another account.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '429':
          $ref: '#/components/responses/RateLimited'

  /withdraw/webhook:
    post:
      operationId: setWithdrawWebhook
      summary: 'Configure the withdrawal webhook'
      description: |
        Creates or updates the configuration. The webhook is stored per user and applies to every withdrawal of that account.

        NETTS sends a POST to your URL when a withdrawal reaches a terminal state, with the header `X-Netts-Signature: base64(HMAC-SHA256(secret, body))`. The signature is computed over canonical JSON: keys sorted, no spaces. Verify it against the raw request bytes; if you parse and re-serialise, reproduce the same canonical form.

        Delivery: up to 3 attempts within 21 minutes of the order being created, with roughly 5 minutes between attempts. Any 2xx counts as success; anything else, including a timeout, is a failed attempt. After that delivery stops and the result must be collected by polling the status endpoint. Each order is delivered at most once successfully, but make your handler idempotent on `orderId` anyway.
      tags: [Account, Webhooks]
      parameters:
        - $ref: '#/components/parameters/RealIp'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WithdrawWebhookConfig' }
            example:
              callback_url: https://your-server.example/netts/withdraw-hook
              secret: your_shared_secret_min_8_chars
              enabled: true
      responses:
        '200':
          description: 'The configuration was saved.'
        '400':
          description: 'Invalid `callback_url` or a secret that is too short.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '429':
          $ref: '#/components/responses/RateLimited'
    get:
      operationId: getWithdrawWebhook
      summary: 'Current withdrawal webhook configuration'
      description: 'The secret is never returned — only the `secret_set` flag arrives.'
      tags: [Account, Webhooks]
      parameters:
        - $ref: '#/components/parameters/RealIp'
      responses:
        '200':
          description: 'Webhook configuration.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  callback_url: { type: string, format: uri }
                  enabled: { type: boolean }
                  secret_set: { type: boolean }
                  updated_at: { type: string, format: date-time }
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
    delete:
      operationId: deleteWithdrawWebhook
      summary: 'Disable the withdrawal webhook'
      tags: [Account, Webhooks]
      parameters:
        - $ref: '#/components/parameters/RealIp'
      responses:
        '200':
          description: 'The configuration was deleted and delivery has stopped.'
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }

  /time/add:
    post:
      operationId: hostAddAddress
      summary: 'Add an address to Host Mode'
      description: |
        Registers a TRON address in Host Mode and, optionally, stores a URL for delegation notifications.

        A new address is added **inactive**: switch it on separately through `/time/order` or `/time/infinitystart`. If the address is already registered under your account, the call updates its `callback_url`. The same address cannot be registered under two different accounts.

        **The `infinity` flag.** With it the address is added and switched straight into infinity mode — the same result as calling `/time/add` and then `/time/infinitystart`. But adding and switching on are two separate steps, and only the first is guaranteed. The response reports the result of adding: if the address was added but could not be switched on, you still get the usual `code: 0` with the usual message, and the address simply stays inactive. Switching on is skipped when the balance does not cover one cycle at the current price, when the address is already active, and when it already has an open order.

        **The response is the same with and without the flag** — no extra fields, no extra error codes, and it does not tell you whether the mode was actually switched on. Confirm it with `/time/status`: the address must report `status: "active"` and `mode: "infinity"`. Do not treat `code: 0` from this method as proof that the mode is running.
      tags: [Host Mode]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                api_key:
                  type: string
                  description: 'API key. You may send the `X-API-KEY` header instead.'
                address: { $ref: '#/components/schemas/TronAddress' }
                callback_url:
                  type: string
                  format: uri
                  maxLength: 2048
                  description: 'Switch infinity mode on immediately, saving a separate call.'
                infinity:
                  type: boolean
                  default: false
                  description: 'Switch infinity mode on immediately, saving a separate call.'
              required: [address]
            example:
              api_key: your_api_key
              address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
              callback_url: https://your-server.com/webhook
      responses:
        '200':
          description: |
            The normal response of the endpoint. Success is `code: 0`; an error also arrives with HTTP 200, but with a negative `code` and `data: null`. Possible errors (all with `code: -1`): no key provided, invalid key or non-whitelisted IP, malformed address, rejected `callback_url`, address owned by another account, temporary database error.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HostEnvelope' }
              examples:
                added:
                  summary: 'Address added'
                  value:
                    code: 0
                    msg: Address added to Host Mode successfully
                    data:
                      address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
                      callback_url: https://your-server.com/webhook
                      timestamp: '2026-07-13T05:30:15.123456'
                error:
                  summary: 'An error — HTTP is 200 as well'
                  value:
                    code: -1
                    msg: Invalid API key or IP not in whitelist
                    data: null
        '402':
          description: 'Balance below the 4 TRX minimum (`1004`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }
              example:
                detail: { code: 1004, msg: Insufficient funds. Minimum balance is 4 TRX. Please top up your account. }
        '403':
          description: 'The API key is blocked (`1005`) — contact support.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }
        '422':
          description: |
            The request body failed validation — a required field is missing or has the wrong type.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HostValidationError' }

  /time/order:
    post:
      operationId: hostOrderCycles
      summary: 'Buy cycles and switch the address on'
      description: |
        Buys a given number of delegation cycles for an address and activates Host Mode. If the address is not in Host Mode yet, the order registers it too.

        **Ordering cycles does not charge you upfront.** Each cycle is billed at the moment its energy is delegated: the first delegation of an activation costs 4 TRX, every next one costs 2 TRX if your previous transfer consumed 65,000 energy or less, and 4 TRX if it consumed more. The `amount` in the response is an estimate (cycles × reference price), not the final total.

        The balance is checked before the order is accepted: if it does not cover the order, the order is rejected with code `-2`.

        Only one open order per address is allowed at a time. Purchased cycles are consumed one per delegation; when they run out, the address stops itself.
      tags: [Host Mode]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                api_key: { type: string }
                address: { $ref: '#/components/schemas/TronAddress' }
                cycles: { type: integer, minimum: 1 }
              required: [address, cycles]
            example:
              api_key: your_api_key
              address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
              cycles: 10
      responses:
        '200':
          description: |
            Success is `code: 0`. Errors also arrive with HTTP 200: `-1` — authentication, address format, `cycles` below one, or a temporary database error; `-2` — insufficient balance; `-6` — the address already has an open order, including an infinity one. application/json: schema: { $ref: '#/components/schemas/HostEnvelope' } examples: ok: summary: 'Cycles purchased, address activated' value: code: 0 msg: Cycles purchased and host mode activated successfully data: order_id: 5426 address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE cycles: 10 amount: 23.5 status: active timestamp: '2026-07-13T05:30:15.123456' insufficient: summary: 'Insufficient balance' value: code: -2 msg: 'Insufficient balance. Required: 23.5 TRX, Available: 10.0 TRX' data: null
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HostEnvelope' }
              examples:
                ok:
                  summary: 'Cycles purchased, address activated'
                  value:
                    code: 0
                    msg: Cycles purchased and host mode activated successfully
                    data:
                      order_id: 5426
                      address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
                      cycles: 10
                      amount: 23.5
                      status: active
                      timestamp: '2026-07-13T05:30:15.123456'
                insufficient:
                  summary: 'Insufficient balance'
                  value:
                    code: -2
                    msg: 'Insufficient balance. Required: 23.5 TRX, Available: 10.0 TRX'
                    data: null
        '403':
          description: 'The API key is blocked (`1005`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }
        '422':
          description: 'The request body failed validation.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HostValidationError' }

  /time/infinitystart:
    post:
      operationId: hostInfinityStart
      summary: 'Enable infinity mode'
      description: |
        Enables unlimited mode for an address already in Host Mode: energy keeps being delegated cycle after cycle until you stop the address with `/time/stop`.

        The address must already be in Host Mode and be inactive. The balance must cover at least one cycle at the current price — this is checked before activation.

        Billing is the same as for ordinary cycles: you are charged at the moment of delegation. The 8 TRX daily minimum and the shutdown after two idle days apply in infinity mode as well.

        In `/time/status` such an address reports `cycles_ordered` and `cycles_remaining` as `null` — read that as unlimited, not as no cycles left.
      tags: [Host Mode]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                api_key: { type: string }
                address: { $ref: '#/components/schemas/TronAddress' }
              required: [address]
            example:
              api_key: your_api_key
              address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
      responses:
        '200':
          description: 'Success is `code: 0`; errors arrive with HTTP 200 and a negative `code`.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HostEnvelope' }
        '402':
          description: 'The balance does not cover even one cycle.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }
        '403':
          description: 'The API key is blocked (`1005`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }
        '422':
          description: 'The request body failed validation.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HostValidationError' }

  /time/status:
    post:
      operationId: hostStatusAll
      summary: 'Status of all Host Mode addresses'
      description: |
        State of every address of the account plus a summary and order statistics.

        If `page` and `page_size` are given, the response is paginated and gains a `pagination` object. Without them every address is returned (up to 1000), sorted by creation time, newest first.

        **The three cycle counters mean different things:** `cycles_completed` counts only since the address was last started, `cycles_total_lifetime` counts its whole history, and `current_cycle_no` is the position of the current delegation. They are expected to differ — for one address you may see 83, 115 and 5 at the same time.
      tags: [Host Mode]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                api_key: { type: string }
                page:
                  type: integer
                  minimum: 1
                  description: 'Page number. Requires `page_size`.'
                page_size:
                  type: integer
                  minimum: 1
                  maximum: 100
                  description: 'Addresses per page. Requires `page`.'
            example:
              api_key: your_api_key
              page: 1
              page_size: 50
      responses:
        '200':
          description: 'The API key is blocked (`1005`).'
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/HostEnvelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          summary:
                            type: object
                            properties:
                              total_addresses: { type: integer }
                              active_addresses: { type: integer }
                              infinity_mode_count: { type: integer }
                              total_cycles_ordered:
                                type: integer
                                description: |
                                  Sum of ordered cycles across open orders; infinity orders are excluded.
                              total_open_orders: { type: integer }
                          order_statistics:
                            type: object
                            properties:
                              total_orders: { type: integer }
                              open_orders: { type: integer }
                              closed_orders: { type: integer }
                              total_cycles_in_open_orders: { type: integer }
                              total_delegations: { type: integer }
                              total_amount_spent:
                                type: number
                                description: 'Actually deducted from the balance, in TRX.'
                          addresses:
                            type: array
                            items: { $ref: '#/components/schemas/HostAddressStatus' }
                          pagination:
                            type: object
                            description: 'Present only when `page` and `page_size` are given.'
                            properties:
                              page: { type: integer }
                              page_size: { type: integer }
                              total_items: { type: integer }
                              total_pages: { type: integer }
                              has_next: { type: boolean }
                              has_prev: { type: boolean }
                          timestamp:
                            type: string
                            description: |
                              Arrives WITHOUT a timezone (`2026-07-09T12:34:50.123456`), so it is not RFC 3339 — parse it as server time in UTC.
        '403':
          description: 'The API key is blocked (`1005`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }
        '422':
          description: 'The request body failed validation.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HostValidationError' }

  /time/status/{address}:
    get:
      operationId: hostStatusOne
      summary: 'Status of a single Host Mode address'
      description: 'Unlike the POST version, the key here is passed only in the `X-API-KEY` header.'
      tags: [Host Mode]
      parameters:
        - name: address
          in: path
          required: true
          schema: { $ref: '#/components/schemas/TronAddress' }
      responses:
        '200':
          description: 'The API key is blocked (`1005`).'
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/HostEnvelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          address_info: { $ref: '#/components/schemas/HostAddressStatus' }
                          timestamp:
                            type: string
                            description: |
                              Arrives WITHOUT a timezone (`2026-07-09T12:34:50.123456`), so it is not RFC 3339 — parse it as server time in UTC.
        '403':
          description: 'The API key is blocked (`1005`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }

  /time/stop:
    post:
      operationId: hostStop
      summary: 'Stop Host Mode for an address'
      description: |
        Closes the open orders of the address and moves it to the inactive state. The used-cycle counter is reset and remaining ordered cycles are not carried over. No new energy is delegated until the address is switched on again with `/time/order` or `/time/infinitystart`.

        The address itself stays registered in Host Mode together with its `callback_url` — to remove it entirely use `/time/delete`.

        Stopping is always available, regardless of your balance.
      tags: [Host Mode]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                api_key: { type: string }
                address: { $ref: '#/components/schemas/TronAddress' }
              required: [address]
            example:
              api_key: your_api_key
              address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
      responses:
        '200':
          description: |
            Success is `code: 0`. Errors with HTTP 200: `-1` — authentication, address format or a database error; `-4` — the address is not in Host Mode under your account; `-7` — the mode is already off. application/json: schema: { $ref: '#/components/schemas/HostEnvelope' } examples: ok: summary: 'Stopped' value: code: 0 msg: Host mode stopped successfully, closed 1 order(s) data: address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE orders_closed: 1 status: inactive timestamp: '2026-07-13T05:30:15.123456' alreadyStopped: summary: 'Was already off' value: { code: -7, msg: Host mode is already inactive, data: null }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HostEnvelope' }
              examples:
                ok:
                  summary: 'Stopped'
                  value:
                    code: 0
                    msg: Host mode stopped successfully, closed 1 order(s)
                    data:
                      address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
                      orders_closed: 1
                      status: inactive
                      timestamp: '2026-07-13T05:30:15.123456'
                alreadyStopped:
                  summary: 'Was already off'
                  value: { code: -7, msg: Host mode is already inactive, data: null }
        '403':
          description: 'The API key is blocked (`1005`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }
        '422':
          description: 'The request body failed validation.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HostValidationError' }

  /time/delete:
    post:
      operationId: hostDeleteAddress
      summary: 'Remove an address from Host Mode'
      description: |
        Removes the address from Host Mode entirely, together with its registered `callback_url`. The address can be added again later with `/time/add`.

        Without the `stop` flag the address must be inactive and have no open orders — otherwise the call is refused and nothing changes.

        **The `stop` flag.** With it an active address is stopped and deleted in one call: all open orders are closed, the mode is switched off, the cycle counter is reset, the stop is written to your action history — and only then is the address deleted. Both steps run **in one transaction**: either the address is stopped and deleted, or nothing changes at all. There is no state where the address ends up stopped but still registered. On an address that is already inactive and has no open orders the flag changes nothing.

        Billing is unaffected: stopping refunds nothing and deleting charges nothing. Cycles already paid for stay paid, and delegated energy is reclaimed on its normal schedule.

        Deleting is always available, regardless of your balance.
      tags: [Host Mode]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                api_key: { type: string }
                address: { $ref: '#/components/schemas/TronAddress' }
                stop:
                  type: boolean
                  default: false
                  description: 'Stop the mode before deleting instead of refusing the call.'
              required: [address]
            example:
              api_key: your_api_key
              address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
              stop: true
      responses:
        '200':
          description: |
            Success is `code: 0`. Every error of this method uses `code: -1` and arrives with HTTP 200: authentication, address format, the address is not yours, an attempt to delete an active address without `stop`, open orders without `stop`, or a database error. application/json: schema: { $ref: '#/components/schemas/HostEnvelope' } examples: ok: summary: 'Deleted' value: code: 0 msg: Address deleted successfully data: address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE timestamp: '2026-07-13T05:30:15.123456' active: summary: 'Active address without the stop flag' value: code: -1 msg: Cannot delete an active address. Stop Host mode first data: null
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HostEnvelope' }
              examples:
                ok:
                  summary: 'Deleted'
                  value:
                    code: 0
                    msg: Address deleted successfully
                    data:
                      address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
                      timestamp: '2026-07-13T05:30:15.123456'
                active:
                  summary: 'Active address without the stop flag'
                  value:
                    code: -1
                    msg: Cannot delete an active address. Stop Host mode first
                    data: null
        '403':
          description: 'The API key is blocked (`1005`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }
        '422':
          description: 'The request body failed validation.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HostValidationError' }

  /aml:
    post:
      operationId: createAmlCheck
      summary: 'Screen an address for AML'
      description: |
        Submits an address for AML screening and returns the risk score, risk level and exposure analysis.

        It works in two modes. With `wait: true` the response waits for the result for up to 15 seconds. Without it the response returns immediately with status `pending` and a `client_order_id`, and the result is fetched through `GET /aml/{order_id}`. A typical check takes 2–10 seconds, and polling is not charged extra.

        The Elliptic score runs from 0 to 10, where 0 means no risk. A `null` value means no triggers were detected.
      tags: [AML]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AmlCheckRequest' }
            examples:
              async:
                summary: 'Asynchronous — immediate response'
                value: { address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE, network: trx, provider: elliptic }
              sync:
                summary: 'Synchronous — wait for the result'
                value: { address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE, network: trx, provider: elliptic, wait: true }
      responses:
        '200':
          description: |
            The check was accepted (`pending`) or completed (`completed`) — read `data.status`. application/json: schema: { $ref: '#/components/schemas/AmlCheckResponse' } examples: pending: summary: 'Accepted, fetch the result by polling' value: success: true data: client_order_id: A4C666ABE24BD4A status: pending address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE provider: elliptic price_usdt: 0.98 price_trx: 4.136286 currency: TRX message: AML check order accepted. Use GET /apiv2/aml/A4C666ABE24BD4A to check status. timestamp: '2026-03-10 09:56:31' completed: summary: 'Completed' value: success: true data: client_order_id: A019540900E55CA status: completed address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE provider: elliptic report_language: en risk_score: 0.802904 risk_level: low is_sanctioned: true created_at: '2026-03-10 15:56:28' completed_at: '2026-03-10 15:56:28' timestamp: '2026-03-10 15:56:31'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AmlCheckResponse' }
              examples:
                pending:
                  summary: 'Accepted, fetch the result by polling'
                  value:
                    success: true
                    data:
                      client_order_id: A4C666ABE24BD4A
                      status: pending
                      address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
                      provider: elliptic
                      price_usdt: 0.98
                      price_trx: 4.136286
                      currency: TRX
                      message: AML check order accepted. Use GET /apiv2/aml/A4C666ABE24BD4A to check status.
                    timestamp: '2026-03-10 09:56:31'
                completed:
                  summary: 'Completed'
                  value:
                    success: true
                    data:
                      client_order_id: A019540900E55CA
                      status: completed
                      address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
                      provider: elliptic
                      report_language: en
                      risk_score: 0.802904
                      risk_level: low
                      is_sanctioned: true
                      created_at: '2026-03-10 15:56:28'
                      completed_at: '2026-03-10 15:56:28'
                    timestamp: '2026-03-10 15:56:31'
        '400':
          description: |
            The address is missing or invalid (`4001`); unknown provider or unsupported network (`4002`). application/json: schema: { $ref: '#/components/schemas/AmlErrorEnvelope' } example: success: false error: { code: 4001, msg: Invalid or missing address }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AmlErrorEnvelope' }
              example:
                success: false
                error: { code: 4001, msg: Invalid or missing address }
        '401':
          description: 'The key is missing or invalid.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }
              example:
                detail: { code: -1, msg: API key not provided }
        '402':
          description: 'Insufficient balance (`4020`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AmlErrorEnvelope' }
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          description: 'The provider is unavailable (`5030`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AmlErrorEnvelope' }
              example:
                success: false
                error: { code: 5030, message: Provider elliptic not available }
                timestamp: '2026-03-10 10:00:00'

  /aml/{order_id}:
    get:
      operationId: getAmlCheck
      summary: 'AML check result'
      description: |
        Status and result of a previously submitted check. The main way to collect the result of an asynchronous check. Polling is not charged.
      tags: [AML]
      parameters:
        - name: order_id
          in: path
          required: true
          description: 'The `client_order_id` from the response that created the check.'
          schema: { type: string }
          example: A4C666ABE24BD4A
      responses:
        '200':
          description: 'The state and, if the check is finished, the full result.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AmlCheckResponse' }
        '401':
          description: 'The key is missing or invalid.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }
        '404':
          description: 'The check was not found (`4040`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AmlErrorEnvelope' }
              example:
                success: false
                error: { code: 4040, message: Order not found }
                timestamp: '2026-03-10 10:00:00'

  /aml/history:
    get:
      operationId: getAmlHistory
      summary: 'Check history for an address'
      description: |
        AML check history for a specific address, yours only: checks made by other users for the same address are not visible. Paginated, 100 records per page, newest first.

        Checks with the `skipped` status (inactive addresses) are excluded from the history. The full result of any record is fetched through `GET /aml/{order_id}` by its `client_order_id`.
      tags: [AML]
      parameters:
        - name: address
          in: query
          required: true
          schema: { type: string, minLength: 10, maxLength: 100 }
        - name: network
          in: query
          required: true
          schema: { type: string }
          example: trx
        - name: page
          in: query
          required: false
          schema: { type: integer, minimum: 1, default: 1 }
      responses:
        '200':
          description: 'A page of the history.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AmlHistoryResponse' }
        '400':
          description: 'Unsupported network (`4002`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AmlErrorEnvelope' }
        '401':
          description: 'The key is missing or invalid.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }

  /aml/price:
    get:
      operationId: getAmlPrice
      summary: 'AML check price and whether your balance covers it'
      tags: [AML]
      parameters:
        - name: provider
          in: query
          required: false
          schema: { type: string, enum: [elliptic], default: elliptic }
      responses:
        '200':
          description: 'The provider''s price, the rate, your balance and whether it covers a check.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      provider: { type: string }
                      price_usdt: { type: number }
                      price_trx: { type: number }
                      trx_rate_usd: { type: number }
                      available: { type: boolean }
                      user_balance_trx: { type: number }
                      can_afford: { type: boolean }
                  timestamp: { type: string }
              example:
                success: true
                data:
                  provider: elliptic
                  price_usdt: 0.98
                  price_trx: 3.43
                  trx_rate_usd: 0.285756
                  available: true
                  user_balance_trx: 314.75
                  can_afford: true
                timestamp: '2026-03-10 09:52:05'
        '401':
          description: 'The key is missing or invalid.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }

  /aml/providers:
    get:
      operationId: listAmlProviders
      summary: 'List AML providers with prices'
      description: |
        Prices already include reseller discounts where applicable, and the TRX price is recalculated at the current rate.
      tags: [AML]
      responses:
        '200':
          description: 'Providers and their prices.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      providers:
                        type: array
                        items:
                          type: object
                          properties:
                            provider: { type: string }
                            price_usdt: { type: number }
                            price_trx: { type: number }
                            available: { type: boolean }
                            reason:
                              type: string
                              description: |
                                Reason for unavailability when `available: false` — for example `quota_exhausted`.
                      trx_rate_usd: { type: number }
                      supported_formats:
                        type: array
                        items: { type: string }
                      supported_languages:
                        type: array
                        items: { type: string }
                  timestamp: { type: string }
              example:
                success: true
                data:
                  providers:
                    - { provider: elliptic, price_usdt: 0.98, price_trx: 3.43, available: true }
                  trx_rate_usd: 0.285756
                  supported_formats: [rate, full, md, pdf]
                  supported_languages: [en]
                timestamp: '2026-03-10 09:52:03'
        '401':
          description: 'The key is missing or invalid.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailObject' }

  /subusergen:
    post:
      operationId: createSubUser
      summary: 'Create a sub-user'
      description: |
        Creates a sub-account linked to your account. A sub-user has its own API key, its own deposit address and a **separate balance**.

        What matters about such accounts: the IP whitelist used is the **parent's**, not its own; the sub-user's key starts with the `mock` prefix; the generated deposit address is not activated by default; the `google_id`, `email` and `name` fields are fake, exist only for internal identification and are not real accounts, while the name and tag can be changed in the dashboard.

        A sub-user can use every API method with its own key, and calculations run against its own balance.

        The request body is an empty object `{}`; the `Content-Type` header is still required.
      tags: [Sub-users]
      parameters:
        - $ref: '#/components/parameters/RealIp'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: 'No parameters — the sub-user is linked to the owner of the key.'
            example: {}
      responses:
        '200':
          description: 'The sub-user was created.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SubUser' }
              example:
                user_id: 1379
                google_id: SUB123456789012345678
                email: SUB123456789012345678@gmail.com
                name: SUB123456789012345678
                deposit_address: TXk8rQSAvPvBBNtqSoY6nCfsXWCSSpTVQF
                api_key: mock1234567890abcdef1234567890ab
                referral_code: A2B4K9
                created_at: '2026-03-14T12:00:00'
                referrer_user_id: 42
        '401':
          description: 'The key header is missing.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }
              example:
                detail: X-API-KEY header required
        '403':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }
        '422':
          description: 'The request body failed validation.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HostValidationError' }
        '500':
          description: 'Failed to create the sub-user.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }

  /subuserdel/{user_id}:
    delete:
      operationId: deleteSubUser
      summary: 'Delete a sub-user'
      description: |
        Deletes a sub-user and its related records. **No history is lost:** orders and delegations are reassigned to the parent account, then the sub-user's own records are removed — API key, settings, referral data, deposit address — and a deletion record is stored for auditing. Everything runs in one transaction: all of it, or nothing.

        Only sub-users can be deleted, and only your own: real accounts are not deletable through this method, and an attempt to delete someone else's sub-user is rejected with 403.
      tags: [Sub-users]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: user_id
          in: path
          required: true
          schema: { type: integer }
          example: 1234
      responses:
        '200':
          description: 'The sub-user was deleted and its history reassigned to the parent.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  user_id: { type: integer }
                  google_id: { type: string }
                  parent_user_id: { type: integer }
                  total_reassigned: { type: integer, description: 'How many history records were reassigned.' }
                  total_deleted: { type: integer }
                  message: { type: string }
              example:
                user_id: 1234
                google_id: SUB123456789012345678
                parent_user_id: 42
                total_reassigned: 787
                total_deleted: 8
                message: 'Mock user 1234 (SUB123456789012345678) deleted. History (787 rows) reassigned to parent 42.'
        '400':
          description: 'The user was not found, or is not a sub-user.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }
              example:
                detail: User with id=9999 not found
        '401':
          description: 'The key header is missing.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }
        '403':
          description: 'This is not your sub-user, or the key or IP was not accepted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }
              example:
                detail: 'Not authorized: you can only delete your own SUB-users.'
        '500':
          description: 'Failed to delete the sub-user.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }

  /usdt/{sender}&{receiver}:
    get:
      operationId: calculateUsdtTransferPublic
      summary: 'USDT transfer cost (public, no key)'
      description: |
        Public estimate of a USDT transfer cost: analysis of both addresses, the energy and bandwidth required, and a comparison of burning versus renting. Neither a key nor a whitelisted IP is required.

        **The rate limit is very low — 1 request per second and 60 per minute, and it is shared.** If your application sits behind Cloudflare or another proxy, the limit is effectively shared between every client reaching us through the same edge, so you may hit `429` well before 60 requests a minute. For anything beyond occasional calls use `POST /usdt/analyze` — it allows 50 requests per second per key.

        The addresses are passed in the path, separated by an ampersand.

        **About client timeouts.** The calculator queries TRON nodes live, so under load a single call can take several seconds: set a timeout of at least 15 seconds, better 30 — the usual 10 is too short. On `429` honour `Retry-After` with a little jitter, on 5xx retry no more than 2–3 times with exponential backoff, and cache the result for 30–60 seconds per address pair.

        Every response carries the debug headers `X-Request-ID`, `X-Process-Time` and `X-Kong-Request-Id` — quote them verbatim when contacting support.
      tags: [USDT]
      security: []
      parameters:
        - name: sender
          in: path
          required: true
          description: 'Sender address.'
          schema: { $ref: '#/components/schemas/TronAddress' }
        - name: receiver
          in: path
          required: true
          description: 'Receiver address. Must differ from the sender.'
          schema: { $ref: '#/components/schemas/TronAddress' }
      responses:
        '200':
          description: 'Transfer analysis.'
          headers:
            X-Request-ID:
              description: 'Request id on the application side.'
              schema: { type: string }
            X-Process-Time:
              description: 'Application processing time in milliseconds, excluding the gateway.'
              schema: { type: string }
            X-Kong-Request-Id:
              description: 'Request id on the Kong gateway side.'
              schema: { type: string }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TransferAnalysisResponse' }
        '400':
          description: |
            Malformed address, fewer than two addresses separated by `&` in the path, or the sender is the same as the receiver. application/json: schema: { $ref: '#/components/schemas/ErrorCodeMsg' } example: code: -1 msg: 'Sender and receiver cannot be the same address'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorCodeMsg' }
              example:
                code: -1
                msg: 'Sender and receiver cannot be the same address'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: 'Internal server error.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorCodeMsg' }
      x-rate-limit: 1 request per second and 60 per minute, and the limit is shared

  /usdt/analyze:
    post:
      operationId: calculateUsdtTransfer
      summary: 'USDT transfer cost (authenticated)'
      description: |
        The same as the public GET, but authenticated and with a limit of 50 requests per second. Use this method for production.

        Two forms of authentication are accepted: the `X-API-KEY` header (preferred — as in the rest of the API) or an `Authorization` header carrying `Bearer {key}` or just the key without a prefix. If both are sent, `X-API-KEY` wins. The request IP must be in the key's whitelist.

        **There is no `amount` field.** The calculation gives the cost and resource requirements for a single USDT transfer between two addresses: one TRC-20 transfer consumes about 130,000 energy regardless of the amount. If you need the breakdown for several transfers, multiply the recommended energy by their count on your side.
      tags: [USDT]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sender_address: { $ref: '#/components/schemas/TronAddress' }
                receiver_address:
                  allOf: [{ $ref: '#/components/schemas/TronAddress' }]
                  description: 'Must differ from the sender address.'
              required: [sender_address, receiver_address]
            example:
              sender_address: TFLit1TFohBtT2f8UVCLFVPmZxawxqByYe
              receiver_address: TTKR9aQdJWTgXLK9cmzaDitT5VXE497thL
      responses:
        '200':
          description: 'Transfer analysis — the same structure as the public method returns.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TransferAnalysisResponse' }
        '400':
          description: 'Invalid address, or the two addresses are identical.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorCodeMsg' }
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorCodeMsg' }
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: 'Internal server error.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorCodeMsg' }
      x-rate-limit: 50 requests per second per key

  /address/rotate:
    post:
      operationId: rotateDepositAddress
      summary: 'Rotate the deposit address'
      description: |
        Queues a replacement of the deposit address. The new address is assigned automatically, usually within 5 minutes.

        **Once the rotation completes, the previous address stops accepting deposits.** So withdraw the remaining balance from it before requesting the rotation, and update your address in every external system afterwards.

        You may always rotate your own address; a parent account may rotate its sub-user's; someone else's address cannot be rotated.

        Only one rotation may be queued per user and address type within seven days; a repeat request is rejected with 409.
      tags: [Account]
      parameters:
        - $ref: '#/components/parameters/RealIp'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                user_id:
                  type: integer
                  description: |
                    Whose address to rotate. Without this field, your own. A parent account may name its sub-user.
                address_type:
                  type: string
                  enum: [web, tg]
                  default: web
            example:
              address_type: web
      responses:
        '200':
          description: 'The request was queued.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AddressRotation' }
              example:
                rotation_id: 42
                user_id: 1234
                address_type: web
                old_address: TGjYzgCyPobsNS9n6WcbdLVR9dH7mWqFx7
                status: pending
                message: Address rotation request submitted. New address will be assigned within 5 minutes.
                estimated_completion: '2026-03-14T12:35:00Z'
        '401':
          description: 'The key is missing or invalid.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }
        '403':
          description: 'An attempt to rotate someone else''s address, or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }
        '409':
          description: 'The user already has a rotation queued for this address type.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }

  /address/rotate/status/{rotation_id}:
    get:
      operationId: getAddressRotationStatus
      summary: 'Address rotation status'
      tags: [Account]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: rotation_id
          in: path
          required: true
          schema: { type: integer }
          example: 42
      responses:
        '200':
          description: 'State of the rotation request.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AddressRotation' }
              example:
                rotation_id: 42
                user_id: 1234
                address_type: web
                status: completed
                old_address: TGjYzgCyPobsNS9n6WcbdLVR9dH7mWqFx7
                new_address: TUrLs6mBTfxKgVQmvHtVpXWJqbeNGA3fyC
                created_at: '2026-03-14T12:30:00Z'
                completed_at: '2026-03-14T12:34:15Z'
        '401':
          description: 'The key is missing or invalid.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }
        '404':
          description: 'The request was not found.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }

  /address/rotate/pending:
    get:
      operationId: getPendingAddressRotation
      summary: 'Whether a rotation is queued'
      description: 'Useful for disabling the rotation button in your interface.'
      tags: [Account]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: address_type
          in: query
          required: false
          schema: { type: string, enum: [web, tg], default: web }
      responses:
        '200':
          description: 'Whether a request is queued, and the request itself.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  user_id: { type: integer }
                  address_type: { type: string, enum: [web, tg] }
                  has_pending: { type: boolean }
                  pending_rotation:
                    type: [object, 'null']
                    properties:
                      rotation_id: { type: integer }
                      status: { type: string }
                      created_at: { type: string, format: date-time }
              example:
                user_id: 1234
                address_type: web
                has_pending: true
                pending_rotation:
                  rotation_id: 42
                  status: pending
                  created_at: '2026-03-14T12:30:00Z'
        '401':
          description: 'The key is missing or invalid.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailString' }

  /webhooks:
    post:
      operationId: createWebhookEndpoint
      summary: 'Register a webhook endpoint'
      description: |
        Registers a delivery endpoint and returns a `secret` that is **shown only once** — it signs every notification, so store it immediately. Lost it? Rotate to get a new one.

        There may be at most two endpoints: one `primary` and one `backup`. If no role is given, the first free one is assigned. A third endpoint is rejected with `4090`, a taken role with `4091`.

        URL requirements, validated on creation and on every edit: `https` only; the host must resolve to a public address — loopback, private RFC1918 ranges, link-local including `169.254.169.254` and other non-routable ranges are rejected; credentials in the URL are not allowed; length up to 2048 characters.
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/RealIp'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri, maxLength: 2048 }
                role: { type: string, enum: [primary, backup] }
              required: [url]
            example:
              url: https://your-server.example/netts/delegation-hook
              role: primary
      responses:
        '201':
          description: |
            The endpoint was created. The `secret` in the response is shown only this once.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookEnvelope' }
              example:
                detail:
                  code: 10000
                  status: created
                  data:
                    id: 1
                    url: https://your-server.example/netts/delegation-hook
                    secret: whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
                    role: primary
                    is_active: true
                    created_at: '2026-01-01T00:00:00'
        '400':
          description: 'The URL is unsafe or fails validation (`4000`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '409':
          description: |
            Both endpoints are already registered (`4090`), or the requested role is taken (`4091`) — swap the roles with PATCH or delete the extra one. application/json: schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          description: 'Failed to create the endpoint (`5003`) — retry.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
      x-rate-limit: 5 requests per second and 150 per minute per API key
    get:
      operationId: listWebhookEndpoints
      summary: 'List your delivery endpoints'
      description: 'The secret is never returned here.'
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/RealIp'
      responses:
        '200':
          description: 'Registered endpoints.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookListEnvelope' }
              example:
                detail:
                  code: 10000
                  status: ok
                  data:
                    endpoints:
                      - { id: 1, url: 'https://your-server.example/netts/delegation-hook', role: primary, is_active: true, created_at: '2026-01-01T00:00:00', updated_at: '2026-01-01T00:00:00' }
                      - { id: 2, url: 'https://backup.example/netts/delegation-hook', role: backup, is_active: true, created_at: '2026-01-01T00:00:00', updated_at: '2026-01-01T00:00:00' }
                    count: 2
                    max_endpoints: 2
                    roles: [primary, backup]
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }

  /webhooks/{id}:
    get:
      operationId: getWebhookEndpoint
      summary: 'A single delivery endpoint'
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: 'Endpoint data, without the secret.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookEnvelope' }
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '404':
          description: 'The endpoint was not found or belongs to another account.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
    patch:
      operationId: updateWebhookEndpoint
      summary: 'Edit a delivery endpoint'
      description: |
        Changes `url`, `is_active` and/or `role` — any subset is allowed, an empty body returns `4220`. A changed URL is validated again.

        **Promoting the backup.** Sending `{"role": "primary"}` to the backup endpoint **swaps the two roles** in a single transaction: the old primary becomes the backup. You are never left without a primary, and no second call is needed. This also gives you a zero-downtime URL change: register the new address as `backup`, verify it, then promote it to `primary`.

        `is_active: false` pauses delivery without deleting the endpoint. Pausing the primary does NOT promote the backup — delivery still targets the primary; if you want the backup to take over, swap the roles.
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              properties:
                url: { type: string, format: uri, maxLength: 2048 }
                is_active: { type: boolean }
                role: { type: string, enum: [primary, backup] }
            example:
              url: https://your-server.example/netts/new-hook
              is_active: false
      responses:
        '200':
          description: 'The endpoint was updated.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookEnvelope' }
        '400':
          description: 'The new URL failed validation (`4000`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '404':
          description: 'The endpoint was not found or belongs to another account.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '422':
          description: 'Nothing to update — empty body (`4220`).'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
    delete:
      operationId: deleteWebhookEndpoint
      summary: 'Delete a delivery endpoint'
      description: 'Hard-deletes the endpoint and frees its role.'
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '204':
          description: 'Deleted. No body.'
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '404':
          description: 'The endpoint was not found or belongs to another account.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }

  /webhooks/{id}/rotate-secret:
    post:
      operationId: rotateWebhookSecret
      summary: 'Rotate the signing secret'
      description: |
        Issues a new secret and shows it once. The new secret takes effect for subsequent deliveries immediately, nothing else is needed. Each endpoint has its own secret: rotating the primary's does not affect the backup's.
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: 'The new secret — shown only this once.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookSecretEnvelope' }
              example:
                detail:
                  code: 10000
                  status: rotated
                  data:
                    id: 1
                    secret: whsec_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
        '401':
          description: 'The key is invalid or the IP is not whitelisted.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }
        '404':
          description: 'The endpoint was not found or belongs to another account.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorDetailStatusObject' }

# Исходящие уведомления: NETTS сам делает запрос К ВАМ. В OpenAPI 3.1 для
# этого есть отдельная секция верхнего уровня — операции ниже описывают
# запрос, который придёт на ваш адрес, а не метод, который вызываете вы.
  /balances/{address}:
    get:
      operationId: getBalanceCurrent
      summary: 'Current balances of an address'
      description: |
        Returns every token held by the address with its symbol, amount and USD value where a price is known. Tokens are ordered by relevance: TRX and major stablecoins first, then verified tokens with a price, then the rest.
        The `balance` field is not the chain's balance at this second: it is the ledger value at the block shown in `as_of_block`. Balances are rebuilt from indexed chain events, and that index advances only to a block every writer has confirmed, so it trails the chain head — measured over 20 minutes on 6 September 2026: about 1 minute at best, 2 minutes typically, and up to 6 minutes at the peak of its sawtooth. On an address that moved funds within that window the amount can be off in either direction. `live=true` applies the remaining tail of transfer events for 30-80 ms, which narrows the gap without closing it, since it covers neither fees nor TRC10 tokens. The field that always holds the chain's present value is `node_balance`. On an address quiet for longer than the current lag there is nothing to trail and the answer is exact.
        Historical endpoints do not have this caveat at all: `/at` and `/at-block` describe a point the index passed long ago.
      tags: [Balances]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: address
          in: path
          required: true
          description: 'TRON address in base58, exactly 34 characters.'
          schema: { type: string, minLength: 34, maxLength: 34 }
          example: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        - name: live
          in: query
          required: false
          description: 'Apply the tail of transfer events between `as_of_block` and the chain head on the fly. Narrows the gap to the chain but does not close it: `as_of_block` does not move, fees are not accounted for, and TRC10 tokens are skipped. Adds 30-80 ms.'
          schema: { type: boolean, default: false }
        - name: hide_spam
          in: query
          required: false
          description: 'Hide tokens classified as deceptive. TRX and USDT are never hidden.'
          schema: { type: boolean, default: false }
      responses:
        '200':
          description: 'Balances of the address.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BalanceList' }
              example:
                status: success
                code: 0
                msg: ''
                data:
                  address: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
                  as_of_block: 86012345
                  live: false
                  hide_spam: false
                  total_value_usd: '385.25'
                  balances:
                    - token_id: TRX
                      symbol: TRON
                      token_type: TRX
                      decimals: 6
                      balance: '1141.899000'
                      price_usd: '0.334968'
                      value_usd: '382.50'
                      is_verified: true
                      is_spam: false
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/BalanceRequired' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/EngineUnavailable' }

  /balances/{address}/at:
    get:
      operationId: getBalanceAtDate
      summary: 'Balances at the end of a date'
      description: |
        Returns the balances as they stood at the **end** of the requested date in UTC, inclusive. A request for `2026-09-01` answers with the state at `2026-09-01 23:59:59Z`, not at midnight.
        Unlike the current-balances endpoint, this one carries no freshness caveat: the point being described was passed long ago, so the answer is exact.
        Use `/at-block` when you need a precise moment: on TRON a block appears roughly every three seconds, so a whole day is a coarse cut.
      tags: [Balances]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: address
          in: path
          required: true
          description: 'TRON address in base58, exactly 34 characters.'
          schema: { type: string, minLength: 34, maxLength: 34 }
          example: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        - name: on
          in: query
          required: true
          description: 'Date in UTC, `YYYY-MM-DD`.'
          schema: { type: string, format: date }
          example: '2026-09-01'
        - name: token_id
          in: query
          required: false
          description: 'Limit the reply to one token. Omit for all tokens.'
          schema: { type: string }
      responses:
        '200':
          description: 'Balances at the end of the date.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BalanceList' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/BalanceRequired' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/EngineUnavailable' }

  /balances/{address}/at-block:
    get:
      operationId: getBalanceAtBlock
      summary: 'Balances at a block or a moment'
      description: |
        Returns the balances at a precise point: either a block height or a UTC timestamp resolved to the nearest block. Exactly one of `block` or `ts` is required.
        The answer is exact for that point. Asking for a block the index has not reached yet is the only case that can surprise you — check `as_of_block` in the reply against what you asked for.
        This is the accurate counterpart of `/at`, which answers for a whole day.
      tags: [Balances]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: address
          in: path
          required: true
          description: 'TRON address in base58, exactly 34 characters.'
          schema: { type: string, minLength: 34, maxLength: 34 }
          example: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        - name: block
          in: query
          required: false
          description: 'Block height. Mutually exclusive with `ts`.'
          schema: { type: integer, minimum: 1 }
          example: 86012345
        - name: ts
          in: query
          required: false
          description: 'Moment in UTC, `YYYY-MM-DD HH:MM:SS`. Mutually exclusive with `block`.'
          schema: { type: string }
          example: '2026-09-01 12:00:00'
      responses:
        '200':
          description: 'Balances at the requested point.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BalanceList' }
        '400':
          description: 'Neither `block` nor `ts` was supplied.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/BalanceRequired' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/EngineUnavailable' }

  /balances/{address}/history:
    get:
      operationId: getBalanceHistory
      summary: 'Balance dynamics for one token'
      description: |
        Returns the balance of one token over time, with the change on each point. Only days on which the balance actually moved are returned, so a quiet address gives a short array rather than a row per day.
        Pass `days` for a trailing window, or `date_from` together with `date_to` for an explicit period. The two date parameters only switch the mode when both are present; supplying one of them is rejected.
      tags: [Balances]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: address
          in: path
          required: true
          description: 'TRON address in base58, exactly 34 characters.'
          schema: { type: string, minLength: 34, maxLength: 34 }
          example: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        - name: token_id
          in: query
          required: false
          description: '`TRX`, a TRC20 contract address, or a numeric TRC10 id.'
          schema: { type: string, default: TRX }
        - name: days
          in: query
          required: false
          description: 'Trailing window in days. Ignored when both dates are given.'
          schema: { type: integer, minimum: 7, maximum: 1825, default: 90 }
        - name: date_from
          in: query
          required: false
          description: 'Start of the period, UTC. Must be sent together with `date_to`.'
          schema: { type: string, format: date }
        - name: date_to
          in: query
          required: false
          description: 'End of the period, UTC, inclusive. Must be sent together with `date_from`.'
          schema: { type: string, format: date }
      responses:
        '200':
          description: 'Points at which the balance changed.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BalanceHistory' }
        '400':
          description: 'Only one of `date_from` / `date_to` was supplied.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/BalanceRequired' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/EngineUnavailable' }

  /balances/{address}/summary:
    get:
      operationId: getBalanceSummary
      summary: 'Period turnover per token'
      description: |
        Returns, for every token the address touched in the period, the opening balance, everything received, everything sent, fees paid and the closing balance. Useful for reconciliation: opening plus inflow minus outflow minus fees equals closing.
        There is no cap on the length of the period beyond `date_to` not preceding `date_from`.
      tags: [Balances]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: address
          in: path
          required: true
          description: 'TRON address in base58, exactly 34 characters.'
          schema: { type: string, minLength: 34, maxLength: 34 }
          example: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        - name: date_from
          in: query
          required: true
          description: 'Start of the period, UTC.'
          schema: { type: string, format: date }
          example: '2026-09-01'
        - name: date_to
          in: query
          required: true
          description: 'End of the period, UTC, inclusive.'
          schema: { type: string, format: date }
          example: '2026-09-30'
      responses:
        '200':
          description: 'Turnover per token.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BalanceSummary' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/BalanceRequired' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/EngineUnavailable' }

  /balances/{address}/statement:
    get:
      operationId: getStatementPreview
      summary: 'Statement preview for one token'
      description: |
        Returns the statement of one token for a period straight away: opening and closing balance, totals, a reconciliation check and the first operations. This is a preview meant for display — the number of operations is capped by `ops_limit`, and `operations_total` tells you how many there are in full.
        For the complete list request a file through `POST /reports/statement`; it is built in the background and downloaded when ready.
      tags: [Balances]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: address
          in: path
          required: true
          description: 'TRON address in base58, exactly 34 characters.'
          schema: { type: string, minLength: 34, maxLength: 34 }
          example: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        - name: token_id
          in: query
          required: false
          description: '`TRX`, a TRC20 contract address, or a numeric TRC10 id.'
          schema: { type: string, default: TRX }
        - name: date_from
          in: query
          required: true
          description: 'Start of the period, UTC.'
          schema: { type: string, format: date }
        - name: date_to
          in: query
          required: true
          description: 'End of the period, UTC, inclusive.'
          schema: { type: string, format: date }
        - name: ops_limit
          in: query
          required: false
          description: 'How many operations to return in the preview.'
          schema: { type: integer, minimum: 10, maximum: 5000, default: 500 }
      responses:
        '200':
          description: 'Statement preview.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatementPreview' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/BalanceRequired' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/EngineUnavailable' }

  /reports/statement:
    post:
      operationId: createStatementReport
      summary: 'Queue a statement file'
      description: |
        Queues a statement file for one token over a period. The reply is immediate and contains an order number; the file itself is built in the background. Poll `GET /reports/{order_no}` or register a webhook to be told when it is ready.
        Repeat calls are safe: send the same `client_request_id`, or an `X-Idempotency-Key` header, and a duplicate returns the existing order instead of creating a second one.
      tags: [Reports]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: X-Idempotency-Key
          in: header
          required: false
          description: 'Your own idempotency key, 12 to 128 characters of `A-Z a-z 0-9 . _ : -`. When omitted, one is derived from the request body.'
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [client_request_id, address, date_from, date_to]
              properties:
                client_request_id: { type: string, maxLength: 128, description: 'Your identifier for this request. Unique per account.' }
                address: { type: string, minLength: 34, maxLength: 34 }
                token_id: { type: string, default: TRX }
                date_from: { type: string, format: date }
                date_to: { type: string, format: date, description: 'Inclusive. The period must not exceed 366 days.' }
                format: { type: string, enum: [csv, pdf], default: csv, description: 'PDF is limited to 5000 operations; use CSV for long periods.' }
            example:
              client_request_id: stmt-2026-09-TXXX-USDT
              address: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
              token_id: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
              date_from: '2026-09-01'
              date_to: '2026-09-30'
              format: csv
      responses:
        '200':
          description: 'Queued, or the already existing order for a repeated request.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportOrder' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/BalanceRequired' }
        '409':
          description: 'The same identifier was used with a different body.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /reports/{order_no}:
    get:
      operationId: getReportStatus
      summary: 'Status of a report order'
      description: |
        Returns the state of an order. While the file is being built the status is `queued`, `running` or `processing`; when it is ready the status becomes `done` and `download_url` appears. A failure gives `error` with a code and a message.
        Orders are visible only to the account that created them.
      tags: [Reports]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: order_no
          in: path
          required: true
          description: 'Order number returned when the report was queued. Always starts with `REP`, which is what keeps this path apart from `/reports/webhooks/...`.'
          schema: { type: string, pattern: '^REP[A-Za-z0-9._:-]+$' }
          example: REPxxxxxxxxxxxx
      responses:
        '200':
          description: 'Order state.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportOrder' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404':
          description: 'No such order for this account.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /reports/{order_no}/download:
    get:
      operationId: downloadReport
      summary: 'Download a finished report'
      description: |
        Streams the finished file. Available only while the order is `done` and has not expired; files are kept for 30 days.
        The response carries `X-Artifact-SHA256` so you can verify the download.
      tags: [Reports]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - name: order_no
          in: path
          required: true
          description: 'Order number. Always starts with `REP`.'
          schema: { type: string, pattern: '^REP[A-Za-z0-9._:-]+$' }
          example: REPxxxxxxxxxxxx
      responses:
        '200':
          description: 'The file.'
          headers:
            X-Artifact-SHA256:
              description: 'SHA-256 of the file, for verification.'
              schema: { type: string }
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404':
          description: 'No such order for this account.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '409':
          description: 'The report is not ready yet.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '410':
          description: 'The report has expired and its file has been removed.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /reports/webhooks:
    post:
      operationId: createReportWebhook
      summary: 'Register a report webhook endpoint'
      description: |
        Registers a URL that NETTS will call when a report is ready. Up to two endpoints per account: `primary`, and an optional `backup` used only after delivery to the primary has been exhausted.
        The signing secret is returned **once, in this reply**. It is never shown again — store it. Lost it, use rotate-secret.
        The URL must be public HTTPS. Loopback, private and link-local addresses are rejected, and so are credentials embedded in the URL.
      tags: [Reports]
      parameters:
        - $ref: '#/components/parameters/RealIp'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, maxLength: 2048, description: 'Public HTTPS URL.' }
                role: { type: string, enum: [primary, backup], description: 'Omit to take whichever role is free.' }
            example: { url: 'https://example.com/netts/reports', role: primary }
      responses:
        '201':
          description: 'Registered. The `secret` field appears only here.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportWebhookCreated' }
        '400':
          description: 'The URL is not a public HTTPS address.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409':
          description: 'Both roles are already taken, or the requested role is occupied.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '429': { $ref: '#/components/responses/RateLimited' }
    get:
      operationId: listReportWebhooks
      summary: 'List your report webhook endpoints'
      description: |
        Returns your registered endpoints. Secrets are never included.
      tags: [Reports]
      parameters:
        - $ref: '#/components/parameters/RealIp'
      responses:
        '200':
          description: 'Your endpoints.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportWebhookList' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /reports/webhooks/{endpoint_id}:
    get:
      operationId: getReportWebhook
      summary: 'Read one endpoint'
      description: 'Returns one of your endpoints. The secret is not included.'
      tags: [Reports]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - { name: endpoint_id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: 'The endpoint.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportWebhook' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: 'No such endpoint for this account.', content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } } }
    patch:
      operationId: patchReportWebhook
      summary: 'Change the URL or pause an endpoint'
      description: |
        Changes the URL, the active flag, or both. A paused endpoint keeps its secret and receives nothing until it is switched back on.
      tags: [Reports]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - { name: endpoint_id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, maxLength: 2048 }
                is_active: { type: boolean }
            example: { is_active: false }
      responses:
        '200':
          description: 'Updated.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportWebhook' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: 'No such endpoint for this account.', content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } } }
        '422': { description: 'The body carried nothing to change.', content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } } }
    delete:
      operationId: deleteReportWebhook
      summary: 'Delete an endpoint'
      description: 'Removes the endpoint. Deliveries already queued to it stop.'
      tags: [Reports]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - { name: endpoint_id, in: path, required: true, schema: { type: integer } }
      responses:
        '204': { description: 'Deleted. No body.' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: 'No such endpoint for this account.', content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } } }

  /reports/webhooks/{endpoint_id}/rotate-secret:
    post:
      operationId: rotateReportWebhookSecret
      summary: 'Issue a new signing secret'
      description: |
        Replaces the signing secret and returns the new one **once**. The old secret stops working immediately, so deploy the new value before rotating if you cannot tolerate a gap.
      tags: [Reports]
      parameters:
        - $ref: '#/components/parameters/RealIp'
        - { name: endpoint_id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: 'Rotated. The `secret` field appears only here.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportWebhookCreated' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: 'No such endpoint for this account.', content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } } }

webhooks:

  delegation.confirmed:
    post:
      operationId: onDelegationConfirmed
      summary: 'Bandwidth order fulfilled'
      description: |
        Sent when an energy rental (`1h` or `5m`) is confirmed on-chain.

        **Delivery is at-least-once.** A dropped response causes a retry, so you may receive the same event twice. Because the action on your side is usually money-sensitive, three things are mandatory: deduplication by `delivery_id`, signature verification BEFORE any money action, and returning 2xx only after the event is durably stored — otherwise we retry, quite correctly.

        **Order of attempts.** Retries first go to the `primary`; the window depends on the order type — about a minute for `5m` and about ten minutes for the rest. When the window is exhausted and a `backup` exists, delivery moves there and the schedule starts over, signed with the backup's own secret. Delivery is marked dead only after the backup is exhausted too.

        **Signature.** Headers: `X-Netts-Event` — the event type, `X-Netts-Delivery` — the `delivery_id`, `X-Netts-Timestamp` — send time in seconds, `X-Netts-Signature` — `sha256=<hex>` where `hex = HMAC_SHA256(secret, "<timestamp>." + raw body)`, and `User-Agent: netts-webhook/1.0`. The scheme follows Stripe's. Compute it over the raw bytes, compare in constant time, and reject if `X-Netts-Timestamp` is outside a ±5 minute window — that is the replay protection. Sign with the secret of THE endpoint the request arrived at: primary and backup have separate secrets.

        Only success events are delivered. Failures and timeouts are never sent — a failed order produces no notification.
      tags: [Webhooks]
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DelegationConfirmed' }
            example:
              event: delegation.confirmed
              delivery_id: 1
              order_id: 1Hxxxxxxxxxx
              order_type: 1h
              receive_address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
              energy_amount: 65000
              tx_hash: '0000000000000000000000000000000000000000000000000000000000000000'
              tx_hashes: ['0000000000000000000000000000000000000000000000000000000000000000']
              delegation_timestamp: 1700000000000
              confirmed_at: '2026-01-01T00:00:00Z'
      responses:
        '200':
          description: 'Any 2xx acknowledges receipt.'

  bandwidth.delegated:
    post:
      operationId: onBandwidthDelegated
      summary: 'Bandwidth order fulfilled'
      tags: [Webhooks]
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BandwidthDelegated' }
            example:
              event: bandwidth.delegated
              delivery_id: 2
              order_id: B1Hxxxxxxxxxxxxx
              order_type: bandwidth
              rental_label: 1h
              rental_seconds: 3600
              receive_address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
              bandwidth_amount: 400
              fulfillment: delegated
              tx_hashes: ['0000000000000000000000000000000000000000000000000000000000000000']
              confirmed_at: '2026-01-01T00:00:00Z'
      responses:
        '200':
          description: 'Any 2xx acknowledges receipt.'

  activation.confirmed:
    post:
      operationId: onActivationConfirmed
      summary: 'Address activated on-chain'
      description: |
        Sent only for real activations: if the address turned out to be already active and no transaction was made, no notification is sent at all.

        An energy order that also required an activation produces **two** notifications — `activation.confirmed` and `delegation.confirmed`. They are separate events with separate `delivery_id`s; route them by the `event` field.
      tags: [Webhooks]
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ActivationConfirmed' }
            example:
              event: activation.confirmed
              delivery_id: 3
              order_id: '123456'
              order_type: activation
              address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
              activation_type: ACC_CREATE
              source: telegram_bot
              tx_hashes: ['0000000000000000000000000000000000000000000000000000000000000000']
              confirmed_at: '2026-01-01T00:00:00Z'
      responses:
        '200':
          description: 'Any 2xx acknowledges receipt.'

  withdraw.completed:
    post:
      operationId: onWithdrawCompleted
      summary: 'Withdrawal reached a terminal state'
      description: |
        Sent to the `callback_url` configured through `POST /withdraw/webhook` when a withdrawal completes or fails. The configuration is stored per user and applies to every withdrawal of the account.

        **Signature** — the header `X-Netts-Signature: base64(HMAC-SHA256(secret, body))`. It is computed over canonical JSON: keys sorted, separators without spaces. Verify against the raw bytes; if you parse and re-serialise, reproduce the same canonical form.

        **Delivery:** up to 3 attempts within 21 minutes of the order being created, with about 5 minutes between them. Success is any 2xx; anything else, including a timeout, counts as a failure. Once the attempts are exhausted delivery stops and the result must be collected through `/withdraw/status/{orderId}`. Each order is delivered at most once successfully, but make your handler idempotent on `orderId` anyway.

        Note that the signature here is computed differently from the order webhooks: without a timestamp in the signed string, and in base64 rather than hex.
      tags: [Webhooks, Account]
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WithdrawWebhookPayload' }
            example:
              orderId: EXAMPLEorderId0000000000000000000000000000Aa
              status: completed
              amount: 15.0
              fee: 1.0
              net: 14.0
              address: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE
              processed_at: '2026-01-01 00:00:00+00:00'
              error_message: null
      responses:
        '200':
          description: 'Any 2xx acknowledges receipt.'

  host.delegation:
    get:
      operationId: onHostDelegation
      summary: 'Host Mode delegation'
      description: |
        The Host Mode notification. It differs from the others: it is a **GET with query-string parameters**, not a POST with JSON, and it is **not signed**. It arrives at the `callback_url` registered through `/time/add` on every energy delegation to the address.

        The timeout is about 10 seconds with up to 3 attempts; if all of them fail the notification is dropped — the delegation itself happens regardless. Notifications can be missed, so your handler must be idempotent and you should reconcile state by polling `/time/status`.
      tags: [Webhooks, Host Mode]
      parameters:
        - name: address
          in: query
          required: true
          schema: { $ref: '#/components/schemas/TronAddress' }
        - name: order_id
          in: query
          required: true
          schema: { type: string }
          example: T149936
        - name: hash
          in: query
          required: true
          schema: { $ref: '#/components/schemas/TxHash' }
        - name: balance_after
          in: query
          required: true
          description: 'Account balance right after this charge — a snapshot taken at charge time.'
          schema: { type: string }
        - name: idle_cycle
          in: query
          required: true
          description: |
            `1` — an idle re-delegation after 24 hours with no transfer, `0` — a regular cycle.
          schema: { type: string, enum: ['0', '1'] }
        - name: energy_used
          in: query
          required: false
          description: |
            The tariff band of the previous transfer. **The key is omitted from the query string entirely** when there is nothing to measure: the first delegation of an activation, any idle re-delegation, and an address with no history. All of those are charged at the 4 TRX rate. A missing key is not an error, and it has no default value.
          schema: { type: string, enum: ['65k', '131k'] }
        - name: charged
          in: query
          required: true
          description: 'Charged for this cycle: `2.0000` or `4.0000`. Always present.'
          schema: { type: string }
      responses:
        '200':
          description: 'Reply with 200 to acknowledge receipt.'
