openapi: 3.1.0
info:
  title: TRC20 Payment Gateway API
  version: 0.3.0
  description: |
    Multi-tenant USDT-TRC20 order and withdrawal-intent API. This API never
    accepts private keys, source addresses, raw transactions, fee limits, or
    arbitrary signing requests. All `/v1` requests use the signed headers below.
servers:
  - url: https://api.auroraex.wickedapp.xyz
    description: TRON Nile staging. Production is issued separately after go-live approval.
tags:
  - name: Health
  - name: Capabilities
  - name: Deposit orders
  - name: Withdrawals
security:
  - mutualTLS: []
    Ed25519Request: []
paths:
  /health/live:
    get:
      tags: [Health]
      operationId: getHealthLive
      security:
        - mutualTLS: []
      summary: Process liveness
      responses:
        "200":
          description: Process is alive.
          headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Health"}
  /health/ready:
    get:
      tags: [Health]
      operationId: getHealthReady
      security:
        - mutualTLS: []
      summary: Database readiness
      responses:
        "200":
          description: Service is ready.
          headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Health"}
        "503": {$ref: "#/components/responses/ServiceUnavailable"}
  /v1/capabilities:
    get:
      tags: [Capabilities]
      operationId: getCapabilities
      summary: Discover fixed gateway capabilities
      x-required-scope: active environment-matched credential
      parameters:
        - {$ref: "#/components/parameters/KeyId"}
        - {$ref: "#/components/parameters/Timestamp"}
        - {$ref: "#/components/parameters/Nonce"}
        - {$ref: "#/components/parameters/Signature"}
      responses:
        "200":
          description: Gateway capabilities. `arbitrary_signing_supported` is always false.
          headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
          content:
            application/json:
              schema:
                type: object
                required: [network, asset, token_decimals, order_ttl_minutes, withdrawals_require_approval, arbitrary_signing_supported]
                properties:
                  network: {type: string, enum: [nile, mainnet]}
                  asset: {type: string, const: USDT_TRC20}
                  token_decimals: {type: integer}
                  order_ttl_minutes: {type: integer}
                  withdrawals_require_approval: {type: boolean, const: true}
                  arbitrary_signing_supported: {type: boolean, const: false}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "409": {$ref: "#/components/responses/Conflict"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "500": {$ref: "#/components/responses/InternalError"}
  /v1/orders:
    post:
      tags: [Deposit orders]
      operationId: createOrder
      summary: Allocate a one-time deposit address
      x-required-scope: orders:write
      parameters:
        - {$ref: "#/components/parameters/KeyId"}
        - {$ref: "#/components/parameters/Timestamp"}
        - {$ref: "#/components/parameters/Nonce"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
        - {$ref: "#/components/parameters/Signature"}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [order_ref, expected_amount]
              properties:
                order_ref: {$ref: "#/components/schemas/Reference"}
                expected_amount: {$ref: "#/components/schemas/Amount"}
                metadata:
                  description: Optional JSON value. Its UTF-8 encoding must not exceed 4 KiB.
      responses:
        "201":
          description: Order created.
          headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Order"}
        "200":
          description: Exact immutable idempotent replay.
          headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Order"}
        "400": {$ref: "#/components/responses/BadRequest"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "409": {$ref: "#/components/responses/Conflict"}
        "413": {$ref: "#/components/responses/PayloadTooLarge"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "500": {$ref: "#/components/responses/InternalError"}
  /v1/orders/{order_ref}:
    get:
      tags: [Deposit orders]
      operationId: getOrder
      summary: Read an order
      x-required-scope: orders:read
      parameters:
        - {$ref: "#/components/parameters/OrderRef"}
        - {$ref: "#/components/parameters/KeyId"}
        - {$ref: "#/components/parameters/Timestamp"}
        - {$ref: "#/components/parameters/Nonce"}
        - {$ref: "#/components/parameters/Signature"}
      responses:
        "200":
          description: Current order state.
          headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Order"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/Conflict"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "500": {$ref: "#/components/responses/InternalError"}
  /v1/withdrawals:
    post:
      tags: [Withdrawals]
      operationId: createWithdrawal
      summary: Submit a withdrawal intent for independent approval
      description: This endpoint never signs or broadcasts synchronously.
      x-required-scope: withdrawals:write
      parameters:
        - {$ref: "#/components/parameters/KeyId"}
        - {$ref: "#/components/parameters/Timestamp"}
        - {$ref: "#/components/parameters/Nonce"}
        - {$ref: "#/components/parameters/IdempotencyKey"}
        - {$ref: "#/components/parameters/Signature"}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [withdrawal_ref, merchant_user_ref, destination_address, amount]
              properties:
                withdrawal_ref: {$ref: "#/components/schemas/Reference"}
                merchant_user_ref:
                  allOf: [{$ref: "#/components/schemas/Reference"}]
                  description: Stable opaque Merchant-side user/account identifier. Do not send a name, email address, or other direct personal data.
                destination_address: {type: string, pattern: "^T[1-9A-HJ-NP-Za-km-z]{33}$"}
                amount: {$ref: "#/components/schemas/Amount"}
                metadata:
                  description: Optional JSON value. Its UTF-8 encoding must not exceed 4 KiB.
      responses:
        "202":
          description: Intent recorded and awaiting approval.
          headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Withdrawal"}
        "200":
          description: Exact immutable idempotent replay.
          headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Withdrawal"}
        "400": {$ref: "#/components/responses/BadRequest"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "409": {$ref: "#/components/responses/Conflict"}
        "413": {$ref: "#/components/responses/PayloadTooLarge"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "500": {$ref: "#/components/responses/InternalError"}
  /v1/withdrawals/{withdrawal_ref}:
    get:
      tags: [Withdrawals]
      operationId: getWithdrawal
      summary: Read a withdrawal intent
      x-required-scope: withdrawals:read
      parameters:
        - {$ref: "#/components/parameters/WithdrawalRef"}
        - {$ref: "#/components/parameters/KeyId"}
        - {$ref: "#/components/parameters/Timestamp"}
        - {$ref: "#/components/parameters/Nonce"}
        - {$ref: "#/components/parameters/Signature"}
      responses:
        "200":
          description: Current withdrawal state.
          headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Withdrawal"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "404": {$ref: "#/components/responses/NotFound"}
        "409": {$ref: "#/components/responses/Conflict"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "500": {$ref: "#/components/responses/InternalError"}
webhooks:
  gatewayEvent:
    post:
      security: []
      operationId: receiveGatewayEvent
      summary: Signed event delivered to a tenant callback URL
      description: |
        Deliveries are at least once and different events can arrive out of order.
        Consumers deduplicate event IDs, apply withdrawal transition versions monotonically,
        and make terminal completion idempotent by Merchant reference.
      parameters:
        - name: X-Webhook-Id
          in: header
          required: true
          schema: {type: string}
        - name: X-Webhook-Timestamp
          in: header
          required: true
          schema: {type: string, pattern: "^[0-9]+$"}
        - name: X-Webhook-Signature
          in: header
          required: true
          schema: {type: string, contentEncoding: base64}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/GatewayEvent"}
      responses:
        "2XX": {description: Acknowledged after the Merchant commits the event; any other response is retried.}
components:
  securitySchemes:
    mutualTLS:
      type: mutualTLS
      description: |
        A gateway-issued client certificate trusted by the Merchant API edge is
        required for every request, including health checks. Certificate
        provisioning and rotation are managed out of band.
    Ed25519Request:
      type: apiKey
      in: header
      name: X-Signature
      description: |
        Base64 Ed25519 signature of `METHOD\nPATH_WITH_QUERY\nTIMESTAMP\nNONCE\nIDEMPOTENCY_KEY\nSHA256_BODY`.
        The associated public key is selected by `X-Key-Id`. GET requests use an empty idempotency-key line.
  parameters:
    KeyId: {name: X-Key-Id, in: header, required: true, schema: {type: string, maxLength: 96}}
    Timestamp: {name: X-Timestamp, in: header, required: true, schema: {type: string, pattern: "^[0-9]+$"}}
    Nonce: {name: X-Nonce, in: header, required: true, schema: {type: string, minLength: 16, maxLength: 128}}
    IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, minLength: 1, maxLength: 128}}
    Signature: {name: X-Signature, in: header, required: true, schema: {type: string, contentEncoding: base64}}
    OrderRef: {name: order_ref, in: path, required: true, schema: {$ref: "#/components/schemas/Reference"}}
    WithdrawalRef: {name: withdrawal_ref, in: path, required: true, schema: {$ref: "#/components/schemas/Reference"}}
  responses:
    BadRequest:
      description: Invalid input or JSON shape.
      headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
      content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
    Unauthorized:
      description: Invalid/stale credentials or a source IP outside the tenant allowlist.
      headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
      content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
    Forbidden:
      description: The authenticated credential lacks the required operation scope.
      headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
      content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
    NotFound:
      description: Tenant-scoped resource not found.
      headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
      content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
    Conflict:
      description: Reused nonce, immutable-reference conflict, or exhausted address pool.
      headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
      content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
    PayloadTooLarge:
      description: HTTP request body exceeds 64 KiB.
      headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
      content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
    RateLimited:
      description: Per-credential rate limit exceeded.
      headers:
        X-Request-Id: {$ref: "#/components/headers/RequestId"}
        Retry-After: {$ref: "#/components/headers/RetryAfter"}
      content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
    InternalError:
      description: Unexpected service or dependency failure.
      headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
      content: {application/json: {schema: {$ref: "#/components/schemas/Error"}}}
    ServiceUnavailable:
      description: Dependency unavailable.
      headers: {X-Request-Id: {$ref: "#/components/headers/RequestId"}}
      content: {application/json: {schema: {$ref: "#/components/schemas/Health"}}}
  headers:
    RequestId:
      description: Correlation identifier to include in support requests.
      schema: {type: string, maxLength: 64}
    RetryAfter:
      description: Minimum whole seconds before another attempt.
      schema: {type: integer, minimum: 1}
  schemas:
    Reference: {type: string, pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"}
    Amount: {type: string, pattern: "^[0-9]+(\\.[0-9]{1,6})?$", examples: ["10.500000"]}
    Health:
      type: object
      required: [status]
      properties: {status: {type: string}}
    Error:
      type: object
      required: [error, request_id]
      properties:
        error: {type: string}
        request_id: {type: string}
    Order:
      type: object
      required: [order_ref, currency, deposit_address, expected_amount, received_total, status, created_at, expires_at]
      properties:
        order_ref: {$ref: "#/components/schemas/Reference"}
        currency: {type: string, const: USDT_TRC20}
        deposit_address: {type: string}
        expected_amount: {$ref: "#/components/schemas/Amount"}
        received_total: {type: string}
        status: {type: string, enum: [pending, partially_paid, paid, underpaid, overpaid, expired, manual_review]}
        idempotent_replay: {type: boolean}
        created_at: {type: string, format: date-time}
        expires_at: {type: string, format: date-time}
    Withdrawal:
      type: object
      required: [withdrawal_ref, merchant_user_ref, currency, destination_address, amount, status, scheduled_at, created_at, updated_at]
      properties:
        withdrawal_ref: {$ref: "#/components/schemas/Reference"}
        merchant_user_ref:
          type: [string, "null"]
          pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
          description: Merchant-side user/account reference. Null only for withdrawals created before attribution was enforced.
        currency: {type: string, const: USDT_TRC20}
        destination_address: {type: string}
        amount: {$ref: "#/components/schemas/Amount"}
        status: {$ref: "#/components/schemas/WithdrawalResourceStatus"}
        tx_id: {type: [string, "null"]}
        idempotent_replay: {type: boolean}
        scheduled_at: {type: [string, "null"], format: date-time}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    GatewayEvent:
      oneOf:
        - {$ref: "#/components/schemas/DepositConfirmedEvent"}
        - {$ref: "#/components/schemas/OrderExpiredEvent"}
        - {$ref: "#/components/schemas/SweepConfirmedEvent"}
        - {$ref: "#/components/schemas/SweepManualReviewEvent"}
        - {$ref: "#/components/schemas/WithdrawalStatusChangedEvent"}
        - {$ref: "#/components/schemas/WithdrawalConfirmedEvent"}
      discriminator:
        propertyName: event
        mapping:
          deposit.confirmed: "#/components/schemas/DepositConfirmedEvent"
          order.expired: "#/components/schemas/OrderExpiredEvent"
          sweep.confirmed: "#/components/schemas/SweepConfirmedEvent"
          sweep.manual_review: "#/components/schemas/SweepManualReviewEvent"
          withdrawal.status_changed: "#/components/schemas/WithdrawalStatusChangedEvent"
          withdrawal.confirmed: "#/components/schemas/WithdrawalConfirmedEvent"
    DepositConfirmedEvent:
      type: object
      additionalProperties: false
      required: [event, event_id, order_ref, currency, amount, received_total, status, tx_id, block_number]
      properties:
        event: {type: string, const: deposit.confirmed}
        event_id: {type: string}
        order_ref: {$ref: "#/components/schemas/Reference"}
        currency: {type: string, const: USDT_TRC20}
        amount: {$ref: "#/components/schemas/Amount"}
        received_total: {$ref: "#/components/schemas/Amount"}
        status: {type: string, enum: [partially_paid, paid, overpaid, manual_review]}
        tx_id: {type: string}
        block_number: {type: integer, minimum: 0}
    OrderExpiredEvent:
      type: object
      additionalProperties: false
      required: [event, event_id, order_ref, currency, received_total, status]
      properties:
        event: {type: string, const: order.expired}
        event_id: {type: string}
        order_ref: {$ref: "#/components/schemas/Reference"}
        currency: {type: string, const: USDT_TRC20}
        received_total: {$ref: "#/components/schemas/Amount"}
        status: {type: string, enum: [expired, underpaid]}
    SweepConfirmedEvent:
      type: object
      additionalProperties: false
      required: [event, event_id, order_ref, currency, amount, amount_atomic, tx_id, destination_address, collection_route_version]
      properties:
        event: {type: string, const: sweep.confirmed}
        event_id: {type: string}
        order_ref: {$ref: "#/components/schemas/Reference"}
        currency: {type: string, const: USDT_TRC20}
        amount: {$ref: "#/components/schemas/Amount"}
        amount_atomic: {type: integer, minimum: 1}
        tx_id: {type: string}
        destination_address: {type: string, pattern: "^T[1-9A-HJ-NP-Za-km-z]{33}$"}
        collection_route_version: {type: integer, minimum: 1}
    SweepManualReviewEvent:
      type: object
      additionalProperties: false
      required: [event, event_id, order_ref, currency, amount, reason]
      properties:
        event: {type: string, const: sweep.manual_review}
        event_id: {type: string}
        order_ref: {$ref: "#/components/schemas/Reference"}
        currency: {type: string, const: USDT_TRC20}
        amount: {$ref: "#/components/schemas/Amount"}
        reason: {type: string}
    WithdrawalStatusChangedEvent:
      type: object
      additionalProperties: false
      required: [event, event_id, withdrawal_ref, status, previous_status, transition_version, currency, amount, amount_atomic, destination_address, tx_id, occurred_at]
      properties:
        event: {type: string, const: withdrawal.status_changed}
        event_id: {type: string}
        withdrawal_ref: {$ref: "#/components/schemas/Reference"}
        status: {$ref: "#/components/schemas/WithdrawalEventStatus"}
        previous_status:
          oneOf:
            - {$ref: "#/components/schemas/WithdrawalEventStatus"}
            - {type: "null"}
        transition_version: {type: integer, minimum: 1}
        currency: {type: string, const: USDT_TRC20}
        amount: {$ref: "#/components/schemas/Amount"}
        amount_atomic: {type: integer, minimum: 1}
        destination_address: {type: string, pattern: "^T[1-9A-HJ-NP-Za-km-z]{33}$"}
        tx_id: {type: [string, "null"]}
        occurred_at: {type: string, format: date-time}
    WithdrawalConfirmedEvent:
      type: object
      additionalProperties: false
      required: [event, event_id, withdrawal_ref, currency, amount_atomic, destination_address, tx_id]
      properties:
        event: {type: string, const: withdrawal.confirmed}
        event_id: {type: string}
        withdrawal_ref: {$ref: "#/components/schemas/Reference"}
        currency: {type: string, const: USDT_TRC20}
        amount_atomic: {type: integer, minimum: 1}
        destination_address: {type: string, pattern: "^T[1-9A-HJ-NP-Za-km-z]{33}$"}
        tx_id: {type: string}
    WithdrawalResourceStatus:
      type: string
      enum: [received, validating, scheduled, awaiting_approval, manual_review, approved, reserved, signing, retry, prepared, broadcast, confirmed, rejected, failed]
    WithdrawalEventStatus:
      type: string
      description: Target/previous status emitted by withdrawal.status_changed. The internal received-to-validating edge emits no callback.
      enum: [scheduled, awaiting_approval, manual_review, approved, reserved, signing, retry, prepared, broadcast, confirmed, rejected, failed]
