openapi: 3.1.0

# ─────────────────────────────────────────────────────────────────────────────
# CryptoGate Public Merchant API — SINGLE SOURCE OF TRUTH
#
# This file is CANONICAL. Code, docs, SDKs, and plugins are reconciled TO it.
# Governance: docs/sdk/api-contract-governance.md  (the contract is FROZEN).
#
# This spec describes the cleaned-up v1 TARGET (decided: clean break + trim + /v1):
#   - transaction_id everywhere (the old `txid` on create/get is removed)
#   - currency_fiat everywhere (the old `currency` on create/get is removed)
#   - one pagination style: limit + offset, with a { limit, offset, total } block
#   - one curated Transaction schema for create/get/list (capability tokens,
#     client_ip, address_index, and other internal fields are NOT exposed)
#   - all paths under /v1
# Migration status of the Go code → this spec is tracked in api-inconsistencies.md.
# ─────────────────────────────────────────────────────────────────────────────

info:
  title: CryptoGate API
  version: "1.0.1"
  description: >
    Accept Bitcoin, Litecoin, Dogecoin, Dash, Ethereum and ERC-20 tokens.
    Server-to-server, authenticated with a secret API key
    (`Authorization: Bearer sk_live_…` for live, or `sk_test_…` for test mode — same base URL).
    All monetary amounts in API responses are JSON numbers; **webhook** amounts are
    strings to preserve decimal precision (see the `webhooks` section).


    **Rate limits.** Every request is rate-limited per API key in two windows — per minute
    and per hour — by plan tier (min/hour): starter 30/500, business 100/2000, professional
    300/5000, enterprise 1000/20000. Every response carries `X-RateLimit-Limit`,
    `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix seconds) for the per-minute window.
    On `429` the body is `error: "RATE_LIMIT_EXCEEDED"` (either window) with a `Retry-After`
    header (seconds) — clients should retry after it. A separate `429` with
    `error: "MONTHLY_LIMIT_REACHED"` means the plan's monthly transaction quota (not the
    request rate).
  license:
    name: Apache-2.0
  contact:
    name: CryptoGate Developers
    url: https://cryptogate.live

servers:
  - url: https://api.cryptogate.live/v1
    description: >
      Single base URL for both live and test traffic. A live secret key (`sk_live_`)
      settles real funds on mainnet; a test secret key (`sk_test_`) runs in test mode
      with no real funds. There is no separate sandbox host.

security:
  - SecretKey: []

tags:
  - name: Transactions
    description: Create and read merchant payment transactions.
  - name: Market Data
    description: Live prices and the supported-currency catalogue.
  - name: Account
    description: Merchant plan, usage, balances, stats and invoices.
  - name: Billing
    description: Plan upgrades and account top-ups.
  - name: System
    description: Health and hosted-checkout redirect.

# ═════════════════════════════════════════════════════════════════════════════
paths:

  /health:
    get:
      operationId: getHealth
      tags: [System]
      summary: API health check
      security: []
      responses:
        "200":
          description: Service status (intentionally NOT wrapped in the success envelope).
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, examples: ["operational"] }
                  service: { type: string, examples: ["CryptoGate API"] }
                  version: { type: string, examples: ["1.0.0"] }

  /pay:
    get:
      operationId: hostedCheckoutRedirect
      tags: [System]
      summary: Hosted-checkout redirect (browser flow, publishable key)
      description: >
        Browser-facing redirect to the hosted payment page, authenticated by a
        **publishable** key in the query string (safe to expose client-side).
        Not used by the server-side SDKs — included for completeness.
      security: []
      parameters:
        - { name: pk, in: query, required: true, schema: { type: string }, description: "Publishable key, pk_live_… / pk_test_…" }
        - { name: amount, in: query, required: true, schema: { type: string }, description: "Fiat amount (≥ 1.00 USD equivalent)." }
        - { name: crypto, in: query, required: true, schema: { $ref: "#/components/schemas/CryptoSymbol" } }
        - { name: link, in: query, required: false, schema: { type: string }, description: "Payment-link slug for attribution." }
      responses:
        "302":
          description: Redirect to https://cryptogate.live/transaction/{transaction_id}
        "400": { $ref: "#/components/responses/BadRequest" }

  /transactions/create:
    post:
      operationId: createTransaction
      tags: [Transactions]
      summary: Create a payment transaction
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateTransactionRequest" }
      responses:
        "201":
          description: Transaction created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransactionEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientBalance" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/ActiveTransactionExists" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /transactions/create-detailed:
    post:
      operationId: createDetailedTransaction
      tags: [Transactions]
      summary: Create an itemized transaction (Professional/Enterprise plans)
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateDetailedTransactionRequest" }
      responses:
        "201":
          description: Transaction created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransactionEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientBalance" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /transactions/list:
    get:
      operationId: listTransactions
      tags: [Transactions]
      summary: List the merchant's transactions (newest first)
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
        - { name: status, in: query, required: false, schema: { $ref: "#/components/schemas/TransactionStatus" } }
        - { name: crypto, in: query, required: false, schema: { $ref: "#/components/schemas/CryptoSymbol" } }
      responses:
        "200":
          description: A page of transactions.
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success: { const: true }
                  data:
                    type: object
                    required: [transactions, pagination]
                    properties:
                      transactions:
                        type: array
                        items: { $ref: "#/components/schemas/Transaction" }
                      pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /transactions/{transaction_id}:
    get:
      operationId: getTransaction
      tags: [Transactions]
      summary: Retrieve a single transaction
      parameters:
        - name: transaction_id
          in: path
          required: true
          schema: { type: string, examples: ["MTX-A1B2C3D4"] }
      responses:
        "200":
          description: The transaction.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransactionEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /prices:
    get:
      operationId: getPrices
      tags: [Market Data]
      summary: Current crypto and fiat exchange rates (USD-denominated)
      responses:
        "200":
          description: Live rates.
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      crypto:
                        type: object
                        additionalProperties: { type: number }
                        description: USD price per unit, keyed by crypto symbol (active coins only).
                      fiat:
                        type: object
                        additionalProperties: { type: number }
                        description: USD value of one unit of each fiat currency.
                      fetched_at: { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "500": { $ref: "#/components/responses/ServerError" }

  /cryptos/list:
    get:
      operationId: listCryptocurrencies
      tags: [Market Data]
      summary: All supported cryptocurrencies and tokens
      responses:
        "200":
          description: Supported-currency catalogue.
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      cryptocurrencies:
                        type: array
                        items: { $ref: "#/components/schemas/Cryptocurrency" }
                      total: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /merchant/cryptos:
    get:
      operationId: listMerchantCryptocurrencies
      tags: [Market Data]
      summary: Cryptocurrencies this merchant has wallets configured for
      responses:
        "200":
          description: The merchant's configured currencies.
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      cryptocurrencies:
                        type: array
                        items: { $ref: "#/components/schemas/Cryptocurrency" }
                      total: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /account:
    get:
      operationId: getAccount
      tags: [Account]
      summary: Merchant plan, usage and limits
      responses:
        "200":
          description: Account details.
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success: { const: true }
                  data: { $ref: "#/components/schemas/Account" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /plans:
    get:
      operationId: listPlans
      tags: [Account]
      summary: Plan catalogue and pricing
      responses:
        "200":
          description: Plans.
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      plans:
                        type: array
                        items: { $ref: "#/components/schemas/Plan" }
                      billing_cycles:
                        type: array
                        items: { type: string }
                        examples: [["monthly", "quarterly", "biannual", "yearly", "biennial"]]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /balances:
    get:
      operationId: listBalances
      tags: [Account]
      summary: On-platform balances (for overage/top-up; NOT crypto settlement)
      responses:
        "200":
          description: Balances.
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      balances:
                        type: array
                        items: { $ref: "#/components/schemas/Balance" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /stats:
    get:
      operationId: getStats
      tags: [Account]
      summary: Merchant transaction analytics
      responses:
        "200":
          description: Stats.
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      total_transactions: { type: integer }
                      monthly_transactions: { type: integer }
                      total_volume_fiat: { type: number }
                      unique_customers: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /invoices:
    get:
      operationId: listInvoices
      tags: [Account]
      summary: CryptoGate billing invoices (platform ↔ merchant)
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Invoices.
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success: { const: true }
                  data:
                    type: object
                    properties:
                      invoices:
                        type: array
                        items: { $ref: "#/components/schemas/Invoice" }
                      pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /billing/checkout:
    post:
      operationId: createBillingCheckout
      tags: [Billing]
      summary: Start a plan upgrade or account top-up
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tier]
              properties:
                tier: { $ref: "#/components/schemas/PlanTier" }
                billing_months: { type: integer, minimum: 1, default: 1 }
                payment_method: { type: string, default: "crypto" }
      responses:
        "201":
          description: Billing transaction created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransactionEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientBalance" }
        "403": { $ref: "#/components/responses/Forbidden" }

# ═════════════════════════════════════════════════════════════════════════════
# Webhooks (OpenAPI 3.1 top-level). Every SDK ships a verify() helper for the
# signature scheme below. NOTE: webhook amounts are STRINGS (precision-safe).
webhooks:
  paymentEvent:
    post:
      operationId: paymentWebhook
      summary: Payment lifecycle event delivered to the merchant's webhook URL
      description: >
        Signed with HMAC-SHA256 over the RAW request body. Verify by comparing
        `X-CryptoGate-Signature: sha256=<hex>` to
        `hex(hmac_sha256(webhook_secret, raw_body))` using a constant-time compare.
        Also sent: `X-CryptoGate-Event` (the event type) and `X-Webhook-ID`
        (unique delivery id — use for idempotency).
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookPayload" }
      responses:
        "200":
          description: Acknowledge within 15s. Non-2xx is retried with backoff.

# ═════════════════════════════════════════════════════════════════════════════
components:

  securitySchemes:
    SecretKey:
      type: http
      scheme: bearer
      description: "Secret API key: `Authorization: Bearer sk_live_…` (or `sk_test_…`)."

  parameters:
    Limit:
      name: limit
      in: query
      required: false
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
    Offset:
      name: offset
      in: query
      required: false
      schema: { type: integer, minimum: 0, default: 0 }
    IdempotencyKey:
      name: X-Idempotency-Key
      in: header
      required: false
      schema: { type: string }
      description: >
        Optional unique key for a create request (e.g. a UUID). A retried create with the
        same key returns the original transaction instead of creating a duplicate — send it
        on every create so a network retry can't double-charge the customer.

  schemas:

    CryptoSymbol:
      type: string
      description: Supported coin/token symbol. /cryptos/list is authoritative at runtime.
      enum: [BTC, LTC, DOGE, DASH, ETH, USDT, USDC, DAI, WBTC, SHIB, PEPE, LINK, UNI, AAVE, MKR]

    FiatCurrency:
      type: string
      enum: [USD, PLN, EUR, GBP]
      default: USD

    TransactionStatus:
      type: string
      description: Public transaction status (internal sub-states are mapped to these).
      enum: [pending, awaiting_confirmation, completed, partial, expired, failed]

    PlanTier:
      type: string
      enum: [starter, business, professional, enterprise]

    Metadata:
      type: [object, "null"]
      description: Free-form key/value (≤20 keys, string values ≤500 chars, ≤4 KB total).
      additionalProperties: { type: string }

    LineItem:
      type: object
      required: [name, price]
      properties:
        name: { type: string }
        quantity: { type: integer, minimum: 1, default: 1 }
        price: { type: number }

    CreateTransactionRequest:
      type: object
      required: [crypto, amount]
      properties:
        crypto: { $ref: "#/components/schemas/CryptoSymbol" }
        amount: { type: number, description: "Fiat amount (≥ 1.00 USD equivalent)." }
        currency_fiat: { $ref: "#/components/schemas/FiatCurrency" }
        metadata: { $ref: "#/components/schemas/Metadata" }
        customer_email: { type: [string, "null"], format: email }
        success_url: { type: [string, "null"], format: uri }
        cancel_url: { type: [string, "null"], format: uri }

    CreateDetailedTransactionRequest:
      type: object
      required: [crypto, items, order_id]
      properties:
        crypto: { $ref: "#/components/schemas/CryptoSymbol" }
        currency_fiat: { $ref: "#/components/schemas/FiatCurrency" }
        items:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/LineItem" }
        order_id: { type: string }
        metadata: { $ref: "#/components/schemas/Metadata" }
        customer_email: { type: [string, "null"], format: email }
        success_url: { type: [string, "null"], format: uri }
        cancel_url: { type: [string, "null"], format: uri }

    PaymentSplit:
      type: object
      description: One on-chain payment toward the transaction.
      properties:
        txid: { type: string, description: "On-chain transaction hash (blockchain id, NOT the CryptoGate transaction_id)." }
        amount_crypto: { type: number }
        confirmations: { type: integer }
        status: { type: string }
        detected_at: { type: string, format: date-time }
        confirmed_at: { type: [string, "null"], format: date-time }

    Transaction:
      type: object
      description: >
        The canonical, curated public view of a transaction (same shape from
        create, get and list). Internal fields (capability tokens, client_ip,
        address_index, derivation/db internals) are deliberately NOT exposed.
      required: [transaction_id, status, crypto, amount_fiat, amount_usd, currency_fiat, created_at, expires_at]
      properties:
        transaction_id: { type: string, examples: ["MTX-A1B2C3D4"] }
        status: { $ref: "#/components/schemas/TransactionStatus" }
        type: { type: string, enum: [basic, detailed] }
        crypto: { $ref: "#/components/schemas/CryptoSymbol" }
        deposit_address: { type: string }
        amount_crypto: { type: number }
        amount_fiat: { type: number }
        amount_usd: { type: number }
        amount_paid: { type: number }
        amount_remaining: { type: number }
        currency_fiat: { $ref: "#/components/schemas/FiatCurrency" }
        fiat_to_usd_rate: { type: number }
        exchange_rate: { type: number, description: "USD per unit of crypto, locked at creation." }
        confirmations_required: { type: integer }
        payment_url: { type: string, format: uri }
        order_id: { type: [string, "null"] }
        customer_email: { type: [string, "null"] }
        items:
          type: [array, "null"]
          items: { $ref: "#/components/schemas/LineItem" }
        payments:
          type: array
          description: On-chain payments detected toward this transaction.
          items: { $ref: "#/components/schemas/PaymentSplit" }
        metadata: { $ref: "#/components/schemas/Metadata" }
        success_url: { type: [string, "null"] }
        cancel_url: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }

    TransactionEnvelope:
      type: object
      required: [success, data]
      properties:
        success: { const: true }
        data: { $ref: "#/components/schemas/Transaction" }

    Cryptocurrency:
      type: object
      properties:
        symbol: { $ref: "#/components/schemas/CryptoSymbol" }
        name: { type: string }
        blockchain: { type: string, examples: ["bitcoin", "ethereum"] }
        network: { type: string, examples: ["mainnet"] }
        type: { type: string, enum: [coin, token] }

    Account:
      type: object
      properties:
        plan:
          type: object
          properties:
            tier: { $ref: "#/components/schemas/PlanTier" }
            billing_cycle: { type: string }
            auto_renewal: { type: boolean }
            expires_at: { type: [string, "null"], format: date-time }
        usage:
          type: object
          properties:
            monthly_transactions_used: { type: integer }
            monthly_transaction_limit: { type: [integer, "null"], description: "null = unlimited." }
            overage_cost_per_tx: { type: number }
        limits:
          type: object
          properties:
            api_rate_limit_per_minute: { type: integer }
            api_rate_limit_per_hour: { type: integer }
            webhook_support: { type: boolean }

    Plan:
      type: object
      properties:
        tier: { $ref: "#/components/schemas/PlanTier" }
        monthly_price_usd: { type: [number, "null"] }
        yearly_price_usd: { type: [number, "null"] }
        monthly_transaction_limit: { type: [integer, "null"] }
        overage_cost_per_tx: { type: number }
        webhook_support: { type: boolean }
        webhook_limit: { type: [integer, "null"] }
        ip_whitelist: { type: boolean }
        api_rate_limit_per_minute: { type: integer }
        api_rate_limit_per_hour: { type: integer }
        custom_pricing: { type: boolean }

    Balance:
      type: object
      properties:
        currency: { type: string, examples: ["USD"] }
        available_balance: { type: number }
        pending_balance: { type: number }
        updated_at: { type: string, format: date-time }

    Invoice:
      type: object
      properties:
        transaction_id: { type: string, examples: ["STX-..."] }
        purchase_type: { type: string, examples: ["plan_purchase", "topup"] }
        amount_fiat: { type: number }
        currency_fiat: { $ref: "#/components/schemas/FiatCurrency" }
        status: { type: string }
        crypto_symbol: { $ref: "#/components/schemas/CryptoSymbol" }
        created_at: { type: string, format: date-time }
        confirmed_at: { type: [string, "null"], format: date-time }
        expires_at: { type: string, format: date-time }

    Pagination:
      type: object
      required: [limit, offset, total]
      properties:
        limit: { type: integer }
        offset: { type: integer }
        total: { type: integer }

    WebhookPayload:
      type: object
      description: >
        Payment-event payload. AMOUNTS ARE STRINGS (e.g. "100.00", "0.00109462")
        to preserve decimal precision — parse before arithmetic.
      required: [event, timestamp, transaction_id, status]
      properties:
        event:
          type: string
          enum:
            - payment.created
            - payment.pending
            - payment.completed
            - payment.overpaid
            - payment.partial
            - payment.expired
            - payment.failed
        timestamp: { type: string, format: date-time }
        transaction_id: { type: string }
        status: { $ref: "#/components/schemas/TransactionStatus" }
        currency_crypto: { $ref: "#/components/schemas/CryptoSymbol" }
        currency_fiat: { $ref: "#/components/schemas/FiatCurrency" }
        amount_fiat: { type: string, description: "Decimal string." }
        amount_usd: { type: string, description: "Decimal string." }
        amount_crypto: { type: string, description: "Decimal string." }
        order_id: { type: [string, "null"] }
        receipt_url: { type: [string, "null"], format: uri, description: "Present on completed/overpaid." }
        metadata: { $ref: "#/components/schemas/Metadata" }

    Error:
      type: object
      required: [success, error, message]
      properties:
        success: { const: false }
        error: { type: string, description: "Machine-readable code.", examples: ["AMOUNT_TOO_LOW"] }
        message: { type: string, description: "Human-readable description." }

  headers:
    X-RateLimit-Limit:
      description: Requests allowed per minute for this API key's plan tier.
      schema: { type: integer }
    X-RateLimit-Remaining:
      description: Requests remaining in the current one-minute window.
      schema: { type: integer }
    X-RateLimit-Reset:
      description: Unix time (seconds) when the current rate-limit window resets.
      schema: { type: integer, format: int64 }
    Retry-After:
      description: Seconds to wait before retrying (sent on 429 responses).
      schema: { type: integer }

  responses:
    BadRequest:
      description: Validation error (INVALID_REQUEST, INVALID_CRYPTO, INVALID_AMOUNT, AMOUNT_TOO_LOW, INVALID_CURRENCY, INVALID_METADATA, WALLET_NOT_CONFIGURED, MISSING_ITEMS, MISSING_ORDER_ID, …).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing or invalid API key (UNAUTHORIZED).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: Key lacks permission — e.g. a publishable key on a secret-only endpoint, or plan too low (FORBIDDEN, USE_PAY_ENDPOINT, PLAN_UPGRADE_REQUIRED, NO_ACTIVE_PLAN).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource not found (TRANSACTION_NOT_FOUND).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    InsufficientBalance:
      description: Merchant platform balance too low for overage fees (INSUFFICIENT_BALANCE).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ActiveTransactionExists:
      description: This client IP already has an active transaction (ACTIVE_TRANSACTION_EXISTS). Body includes an `existing_transaction` object.
      content:
        application/json:
          schema:
            allOf:
              - { $ref: "#/components/schemas/Error" }
              - type: object
                properties:
                  existing_transaction:
                    type: object
                    properties:
                      transaction_id: { type: string }
                      status: { $ref: "#/components/schemas/TransactionStatus" }
                      payment_url: { type: string, format: uri }
    TooManyRequests:
      description: >
        Rate limit or quota exceeded. `error` is `RATE_LIMIT_EXCEEDED` (per-minute request
        rate — honour `Retry-After`) or `MONTHLY_LIMIT_REACHED` (the plan's monthly
        transaction quota). Rate-limit headers accompany the `RATE_LIMIT_EXCEEDED` case.
      headers:
        X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
        X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
        X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
        Retry-After: { $ref: "#/components/headers/Retry-After" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ServerError:
      description: Internal error (RATE_FETCH_FAILED, ADDRESS_GENERATION_FAILED, UPSTREAM_ERROR, …).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
