{
  "openapi": "3.1.0",
  "info": {
    "title": "Hodle API",
    "version": "1.0.0",
    "description": "Lightning + PIX in one API. Issue Lightning invoices, run PIX on-ramps and off-ramps, transfer stablecoins, and ship KYC."
  },
  "servers": [
    {
      "url": "https://api.hodle.com.br",
      "description": "Production"
    },
    {
      "url": "https://sandbox-api.hodle.com.br",
      "description": "Sandbox (testnet USDB on Base Sepolia, no real money)"
    }
  ],
  "security": [{ "bearerAuth": [] }],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Authenticate every request with `Authorization: Bearer YOUR_API_KEY`."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": false },
          "error": { "type": "string", "example": "Validation failed" },
          "errorCode": { "type": "string", "nullable": true, "example": "KYC_REQUIRED" },
          "details": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "field": { "type": "string" },
                "message": { "type": "string" }
              }
            }
          }
        }
      },
      "PixKeyType": {
        "type": "string",
        "enum": ["PHONE", "CPF", "EMAIL", "RANDOM", "CNPJ"]
      },
      "Network": {
        "type": "string",
        "enum": ["polygon", "base", "tron", "solana", "lightning"]
      },
      "DepositAssetRequest": {
        "type": "object",
        "required": ["value", "asset"],
        "properties": {
          "value": { "type": "integer", "minimum": 1, "example": 5000, "description": "Amount in BRL cents." },
          "address": { "type": "string", "example": "lnbc500u1pj...", "description": "Destination address. BOLT11 invoice or LNURL email for LIGHTNING, EVM address for USDT, USDC, USDCE, and BRLA." },
          "asset": { "type": "string", "enum": ["LIGHTNING", "USDT", "USDC", "USDCE", "BRLA"], "example": "LIGHTNING" },
          "network": { "type": "string", "enum": ["polygon", "arbitrum", "base", "gnosis", "tron", "solana", "lightning"], "description": "On-chain network. Required when `asset` is `USDT`, `USDC`, `USDCE`, or `BRLA`. Not needed for `LIGHTNING`." },
          "externalId": { "type": "string", "description": "Idempotency key — generated if not provided." },
          "taxId": { "type": "string", "description": "Beneficiary CPF, digits only. Omit for the account holder." },
          "subAccountId": { "type": "string", "description": "Create the deposit on behalf of this subaccount. Defaults to the main account." }
        }
      },
      "DepositAssetResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "externalId": { "type": "string" },
          "qrCode": { "type": "string", "nullable": true },
          "fee": { "type": "number" },
          "fxRateAtTx": { "type": "number" },
          "walletCharge": { "type": "string", "nullable": true }
        }
      },
      "LightningInvoiceRequest": {
        "type": "object",
        "required": ["value"],
        "properties": {
          "value": { "type": "integer", "minimum": 10, "maximum": 25000, "example": 1000, "description": "BRL cents. Min R$0.10, max R$250.00." },
          "pixKey": { "type": "string", "example": "user@email.com" },
          "pixKeyType": { "$ref": "#/components/schemas/PixKeyType" },
          "qrCode": { "type": "string", "description": "PIX BR Code copy-paste — mutually exclusive with `pixKey`." },
          "refundAddress": { "type": "string", "description": "Lightning address to refund to on failure." }
        }
      },
      "LightningInvoiceResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "invoice": { "type": "string", "example": "lnbc32310n1p5u2g2qsp5..." },
          "valueInSatoshis": { "type": "integer", "example": 276190 },
          "pixKey": { "type": "string", "nullable": true },
          "qrCode": { "type": "string", "nullable": true },
          "valueInBrl": { "type": "string", "example": "1000.00" },
          "fee": { "type": "string", "example": "170.00" }
        }
      },
      "WalletKeysResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "data": {
            "type": "object",
            "properties": {
              "protectedSymmetricKey": { "type": "string" },
              "email": { "type": "string", "format": "email" }
            }
          }
        }
      },
      "WalletGetResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "data": {
            "type": "object",
            "properties": {
              "userId": {
                "type": "string"
              },
              "subAccountId": {
                "type": "string",
                "nullable": true,
                "description": "Avenia subaccount the wallet belongs to, when there is one."
              },
              "syncedAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "addresses": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "example": {
                  "polygon": "0x9aB7a1...",
                  "base": "0x9aB7a1...",
                  "tron": "TQn9Y2..."
                }
              },
              "balances": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "asset": {
                      "type": "string"
                    },
                    "network": {
                      "type": "string"
                    },
                    "amount": {
                      "type": "string"
                    }
                  }
                }
              },
              "walletId": {
                "type": "string",
                "description": "Selected wallet ID, returned when walletId was supplied."
              }
            }
          }
        }
      },
      "WalletTransferRequest": {
        "type": "object",
        "required": ["network", "recipientAddress", "amount", "walletPin", "protectedSymmetricKey"],
        "properties": {
          "network": { "type": "string", "enum": ["polygon", "base", "tron"] },
          "recipientAddress": { "type": "string", "example": "0x9aB7a1c2D3e4F5061728394A5B6C7D8E9F0a1B2C" },
          "amount": { "type": "string", "example": "10.00", "description": "USDT amount as a decimal string." },
          "walletPin": { "type": "string", "description": "PIN typed by the end user." },
          "protectedSymmetricKey": { "type": "string", "description": "Returned by `POST /api/wallet/keys`." }
        }
      },
      "WalletTransferResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "data": {
            "type": "object",
            "properties": {
              "txHash": { "type": "string" },
              "network": { "type": "string" },
              "recipientAddress": { "type": "string" },
              "asset": { "type": "string", "example": "USDT" },
              "amount": { "type": "string" }
            }
          }
        }
      },
      "WalletPayoutRequest": {
        "type": "object",
        "required": ["value", "network"],
        "properties": {
          "value": { "type": "integer", "minimum": 10, "example": 5000, "description": "BRL cents. Minimum 10 (R$ 0.10). There is no fixed maximum in the schema — the ceiling is your per-account per-transaction limit (`perTxLimitBrl`, default R$ 3000). Exceeding it returns 403 `PER_TX_LIMIT_EXCEEDED`." },
          "network": { "type": "string", "enum": ["polygon", "base", "tron"], "description": "Tron additionally requires the `TRON_PAYOUT` feature flag." },
          "asset": { "type": "string", "enum": ["USDT", "USDC", "BRLA"], "description": "Funding stablecoin. `polygon`: `USDT` (default) or `BRLA`. `base`: `USDC` (default) or `BRLA`. `tron`: `USDT` only." },
          "pixKey": { "type": "string" },
          "pixKeyType": { "$ref": "#/components/schemas/PixKeyType" },
          "qrCode": { "type": "string", "description": "PIX BR Code — mutually exclusive with `pixKey`." },
          "walletPin": { "type": "string" },
          "protectedSymmetricKey": { "type": "string" },
          "taxId": { "type": "string", "description": "CPF of the beneficiary receiving the PIX, digits only. Omit for the account holder. A different value is a third-party operation and returns 403 unless third-party operations are enabled for the account." },
          "subAccountId": { "type": "string", "description": "Pay out from this subaccount's wallet. `walletPin` and `protectedSymmetricKey` must be the subaccount's. Defaults to the main account." }
        }
      },
      "WalletPayoutResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "transactionId": { "type": "string", "example": "65f1a8..." },
          "status": { "type": "string", "example": "PROCESSING" },
          "stableAmount": { "type": "string", "example": "9.31" },
          "valueInBrl": { "type": "string", "example": "50.00" },
          "fee": { "type": "string", "example": "2.75" },
          "network": { "type": "string", "example": "polygon" },
          "jobId": { "type": "string" }
        }
      },
      "WalletPayoutStatusResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "data": {
            "type": "object",
            "properties": {
              "transactionId": { "type": "string" },
              "status": { "type": "string", "enum": ["PENDING", "COMPLETED", "FAILED", "REFUNDED"] },
              "network": { "type": "string" },
              "txHash": { "type": "string", "nullable": true },
              "endToEndId": { "type": "string", "nullable": true },
              "valueInBrl": { "type": "string" },
              "fee": { "type": "string" },
              "pixKey": { "type": "string", "nullable": true },
              "qrCode": { "type": "string", "nullable": true },
              "provider": { "type": "string" },
              "correlationID": { "type": "string", "nullable": true },
              "failureReason": { "type": "string", "nullable": true },
              "createdAt": { "type": "string", "format": "date-time" },
              "updatedAt": { "type": "string", "format": "date-time" }
            }
          }
        }
      },
      "DepositAssetStatusResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "data": {
            "type": "object",
            "properties": {
              "externalId": { "type": "string" },
              "walletCharge": { "type": "string" },
              "trackId": { "type": "string", "nullable": true },
              "status": { "type": "string", "enum": ["PENDING", "PROCESSING", "COMPLETED", "FAILED", "EXPIRED", "REFUNDED"] },
              "errorCode": {"type": "string", "nullable": true},
              "errorDescription": {"type": "string", "nullable": true},
              "retryable": {"type": "boolean", "description": "True only after a full confirmed refund."},
              "refundStatus": {"type": "string", "nullable": true},
              "refundedValue": {"type": "integer", "nullable": true, "description": "Cumulative confirmed refunds, in BRL cents."},
              "refundedAt": {"type": "string", "format": "date-time", "nullable": true},
              "refundEndToEndId": {"type": "string", "nullable": true},
              "processingStep": { "type": "string", "nullable": true },
              "asset": { "type": "string" },
              "network": { "type": "string" },
              "address": { "type": "string" },
              "value": { "type": "integer", "description": "Amount in BRL cents." },
              "valueInBrl": { "type": "string" },
              "fee": { "type": "integer", "description": "Fee in BRL cents." },
              "feeInBrl": { "type": "string" },
              "fxRateAtTx": { "type": "number", "nullable": true },
              "receivedAmount": { "type": "integer", "nullable": true, "description": "Amount delivered, in the asset's smallest unit." },
              "receivedAmountDecimals": { "type": "integer", "nullable": true },
              "transactionHash": { "type": "string", "nullable": true },
              "qrCode": { "type": "string", "nullable": true },
              "endToEndId": { "type": "string", "nullable": true, "description": "End-to-end id of the PIX that funded the deposit." },
              "paidAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the PIX settled." },
              "payer": {
                "type": "object",
                "nullable": true,
                "description": "Who paid the PIX. Only returned to accounts that own a virtual account.",
                "properties": {
                  "name": { "type": "string", "nullable": true },
                  "taxId": { "type": "string", "nullable": true, "description": "CPF/CNPJ of the payer, digits only." }
                }
              },
              "confirmedAt": { "type": "string", "format": "date-time", "nullable": true },
              "createdAt": { "type": "string", "format": "date-time" },
              "updatedAt": { "type": "string", "format": "date-time" }
            }
          }
        }
      },
      "AccountStatementResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "data": {
            "type": "object",
            "properties": {
              "balances": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "asset": { "type": "string" },
                    "network": { "type": "string", "nullable": true },
                    "amount": { "type": "string" }
                  }
                }
              },
              "operations": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string" },
                    "type": { "type": "string", "enum": ["DEPOSIT_ASSET", "PIX_OUT", "LIGHTNING_OUT", "WALLET_TRANSFER", "FEE"] },
                    "status": { "type": "string" },
                    "asset": { "type": "string" },
                    "network": { "type": "string" },
                    "valueInBrl": { "type": "string" },
                    "stableAmount": { "type": "string", "nullable": true },
                    "fee": { "type": "string" },
                    "txHash": { "type": "string", "nullable": true },
                    "endToEndId": { "type": "string", "nullable": true },
                    "createdAt": { "type": "string", "format": "date-time" },
                    "updatedAt": { "type": "string", "format": "date-time" }
                  }
                }
              },
              "page": { "type": "integer" },
              "pageSize": { "type": "integer" },
              "total": { "type": "integer" }
            }
          }
        }
      },
      "SubAccountCreateRequest": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": { "type": "string", "minLength": 1, "maxLength": 64, "example": "Maria Silva" },
          "email": { "type": "string", "format": "email", "description": "Optional. Identifies the subaccount owner on KYC/KYB and in your statements. Omitting it no longer blocks later calls: the subaccount is always resolvable by `subAccountId`." },
          "accountType": { "type": "string", "enum": ["INDIVIDUAL", "COMPANY"], "default": "INDIVIDUAL" }
        }
      },
      "SubAccountListResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "data": {
            "type": "object",
            "properties": {
              "subAccounts": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "subAccountId": { "type": "string", "example": "883f2eec-0cea-44aa-9554-67055066bbc7" },
                    "email": { "type": "string", "example": "maria@example.com" },
                    "owner": { "type": "string", "enum": ["SELF", "CUSTOMER"] },
                    "createdAt": { "type": "string", "format": "date-time" }
                  }
                }
              }
            }
          }
        }
      },
      "SubAccountCreateResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "data": {
            "type": "object",
            "properties": {
              "subAccountId": { "type": "string", "example": "883f2eec-0cea-44aa-9554-67055066bbc7" }
            }
          }
        }
      },
      "QuoteRequest": {
        "type": "object",
        "required": ["inputCurrency", "inputPaymentMethod", "outputCurrency", "outputPaymentMethod"],
        "description": "Pass exactly one of `inputAmount` or `outputAmount`.",
        "properties": {
          "inputCurrency": { "type": "string", "enum": ["BRL", "USDT", "USDC", "USDB", "BTC"] },
          "inputPaymentMethod": { "type": "string", "enum": ["PIX", "POLYGON", "BASE", "TRON", "LIGHTNING", "SPARK"] },
          "outputCurrency": { "type": "string", "enum": ["BRL", "USDT", "USDC", "USDB", "BTC"] },
          "outputPaymentMethod": { "type": "string", "enum": ["PIX", "POLYGON", "BASE", "TRON", "LIGHTNING", "SPARK"] },
          "inputAmount": { "type": "string", "example": "100.00", "description": "Decimal string." },
          "outputAmount": { "type": "string", "description": "Decimal string. Use for exact-out quotes." },
          "outputBrCode": { "type": "string", "description": "PIX BR Code, when the output goes to a known PIX recipient. Routes and prices this quote only — it grants no third-party permission." }
        }
      },
      "QuoteResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "data": {
            "type": "object",
            "properties": {
              "quoteToken": { "type": "string", "description": "Provider-side reference, returned for traceability. No Hodle endpoint accepts it as input — a quote is indicative pricing, not a rate lock." },
              "pairName": { "type": "string", "example": "BRL-USDC" },
              "basePrice": { "type": "string" },
              "inputCurrency": { "type": "string" },
              "outputCurrency": { "type": "string" },
              "inputAmount": { "type": "string" },
              "outputAmount": { "type": "string" },
              "inputAmountSats": { "type": "integer", "description": "BTC sats required by a BTC to BRL exact-output quote." },
              "outputAmountSats": { "type": "integer", "description": "BTC sats delivered by a BRL to BTC exact-input quote." },
              "feeInCents": { "type": "integer", "description": "Total fee charged to the authenticated API-key owner after custom fee overrides and floors." },
              "appliedFees": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "type": { "type": "string" },
                    "amount": { "type": "string" },
                    "currency": { "type": "string" }
                  }
                }
              },
              "expiresAt": { "type": "string", "format": "date-time", "description": "Two minutes after creation. Marks the price as stale, reserves nothing." }
            }
          }
        }
      },
      "KycCreateRequest": {
        "type": "object",
        "description": "Hosted flow. Omit `subAccountId` to run KYC for the API-key user itself.",
        "properties": {
          "subAccountId": { "type": "string", "description": "Subaccount the KYC belongs to." },
          "level": { "type": "string", "enum": ["LIGHT", "FULL"], "default": "FULL" },
          "redirectUrl": { "type": "string", "format": "uri", "description": "Where the customer lands once the hosted flow finishes." }
        }
      },
      "KycSubmitFormRequest": {
        "type": "object",
        "description": "Form flow, for integrators that collect the data and upload the documents themselves via `POST /api/kyc/document`. If the subaccount was created without an `email`, the one sent here becomes its email on file.",
        "required": ["subAccountId", "fullName", "dateOfBirth", "countryOfTaxId", "taxIdNumber", "email", "country", "state", "city", "zipCode", "streetAddress", "uploadedSelfieId", "uploadedDocumentId"],
        "properties": {
          "subAccountId": { "type": "string" },
          "fullName": { "type": "string" },
          "dateOfBirth": { "type": "string", "example": "1990-01-31", "description": "YYYY-MM-DD." },
          "countryOfTaxId": { "type": "string", "example": "BRA", "description": "ISO 3166-1 alpha-3." },
          "taxIdNumber": { "type": "string", "description": "Digits only." },
          "email": { "type": "string", "format": "email" },
          "phone": { "type": "string" },
          "country": { "type": "string", "example": "BRA" },
          "state": { "type": "string" },
          "city": { "type": "string" },
          "zipCode": { "type": "string" },
          "streetAddress": { "type": "string" },
          "uploadedSelfieId": { "type": "string", "description": "Document id returned by POST /api/kyc/document." },
          "uploadedDocumentId": { "type": "string", "description": "Document id returned by POST /api/kyc/document." }
        }
      },
      "KycCreateResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "data": {
            "type": "object",
            "properties": {
              "attemptId": { "type": "string" },
              "status": { "type": "string", "example": "PENDING" },
              "url": { "type": "string", "format": "uri" },
              "expiresAt": { "type": "string", "format": "date-time", "nullable": true, "description": "Null when the provider does not publish an expiry for the hosted link." }
            }
          }
        }
      },
      "KycImportTokenRequest": {
        "type": "object",
        "required": ["importToken"],
        "properties": {
          "subAccountId": { "type": "string", "description": "Subaccount the verification is imported into. Omit to import into the API-key account itself. Must be an INDIVIDUAL subaccount." },
          "importToken": { "type": "string", "description": "Single-use Sumsub share token, minted on your own Sumsub account for a GREEN applicant with Hodle's provider as `forClientId`. Never stored by Hodle." }
        }
      },
      "KycImportTokenResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "data": {
            "type": "object",
            "properties": {
              "attemptId": { "type": "string" },
              "status": { "type": "string", "example": "PENDING" },
              "source": { "type": "string", "example": "SUMSUB_IMPORT" },
              "createdAt": { "type": "string", "format": "date-time" }
            }
          }
        }
      },
      "KycGetResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "data": {
            "type": "object",
            "properties": {
              "status": { "type": "string", "enum": ["NONE", "PENDING", "REVIEW", "APPROVED", "REJECTED", "EXPIRED", "FAILED"] },
              "level": { "type": "string", "enum": ["LIGHT", "FULL"] },
              "lastAttemptId": { "type": "string", "nullable": true },
              "approvedAt": { "type": "string", "format": "date-time", "nullable": true }
            }
          }
        }
      },
      "WebhookPayload": {
        "type": "object",
        "properties": {
          "event": {
            "type": "string",
            "enum": [
              "DEPOSIT_ASSET_SUCCESS",
              "PAYOUT_SUCCESSFUL",
              "PAYOUT_FAILED",
              "KYC_APPROVED",
              "KYC_REJECTED",
              "KYC_EXPIRED",
              "KYC_FAILED",
              "PAYOUT_REFUNDED",
              "DISPUTE_CREATED",
              "DISPUTE_ACCEPTED",
              "DISPUTE_REJECTED",
              "DISPUTE_CANCELED"
            ]
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "Event-specific payload. See the Webhooks guide for the shape per event."
          }
        }
      }
    }
  },
  "paths": {
    "/api/subaccount": {
      "post": {
        "operationId": "subAccountCreate",
        "summary": "Create a subaccount",
        "description": "Create an end-user subaccount under your API key. Subaccounts are permanent — deletion is not supported. Pass `email` so later KYC/KYB and operation calls can resolve it.",
        "tags": ["Subaccounts"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SubAccountCreateRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Subaccount created.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SubAccountCreateResponse" }
              }
            }
          },
          "400": {
            "description": "Validation failed or the request was rejected upstream.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "401": {
            "description": "Missing key, or key does not match the base URL environment.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "subAccountList",
        "summary": "List your subaccounts",
        "tags": ["Subaccounts"],
        "responses": {
          "200": {
            "description": "Subaccounts scoped to your API key: your own Avenia subaccount (`owner: SELF`) plus the ones you created for customers (`owner: CUSTOMER`).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SubAccountListResponse" }
              }
            }
          },
          "401": {
            "description": "Missing key, or key does not match the base URL environment.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/subaccount/{subAccountId}": {
      "get": {
        "operationId": "subAccountGet",
        "summary": "Read one subaccount",
        "tags": ["Subaccounts"],
        "parameters": [
          { "name": "subAccountId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Subaccount detail." },
          "404": {
            "description": "Not found, or not scoped to your API key.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/quote": {
      "post": {
        "operationId": "quote",
        "summary": "Price a BRL ↔ asset conversion",
        "description": "Indicative pricing and fee breakdown for a pair. The returned `quoteToken` is informational — no endpoint consumes it, so a quote does not lock a rate. Price the operation immediately before executing it.",
        "tags": ["Pricing"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/QuoteRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Quote.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/QuoteResponse" }
              }
            }
          },
          "400": {
            "description": "Validation failed, amount below minimum, or pair not routable.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/kyc": {
      "get": {
        "operationId": "kycGet",
        "summary": "Get current KYC state",
        "description": "State of the API-key user, or of one subaccount when `subAccountId` is given. Answers `200` with `status: NONE` when nothing was ever submitted.",
        "tags": ["KYC"],
        "parameters": [
          { "name": "subAccountId", "in": "query", "schema": { "type": "string" }, "description": "Read the state of this subaccount instead of the API-key user." }
        ],
        "responses": {
          "200": {
            "description": "KYC state.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/KycGetResponse" }
              }
            }
          },
          "404": { "description": "Subaccount not found for this platform." }
        }
      },
      "post": {
        "operationId": "kycCreate",
        "summary": "Start a KYC attempt",
        "description": "Two payloads are accepted on this route. Send `{ level, redirectUrl }` (optionally `subAccountId`) to open the **hosted flow** — the response carries the `url` the customer must visit. Send the **form payload** (`KycSubmitFormRequest`) when you collect and upload the documents yourself; it answers `202` with the attempt id instead of a url.",
        "tags": ["KYC"],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  { "$ref": "#/components/schemas/KycCreateRequest" },
                  { "$ref": "#/components/schemas/KycSubmitFormRequest" }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Hosted KYC attempt created. Send the customer to `data.url`.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/KycCreateResponse" }
              }
            }
          },
          "202": { "description": "Form submission accepted. Poll `GET /api/kyc/{attemptId}` for the result." },
          "400": { "description": "Validation error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthorized." }
        }
      }
    },
    "/api/kyc/import-token": {
      "post": {
        "operationId": "kycImportToken",
        "summary": "Import an existing Sumsub verification",
        "description": "Reuse a KYC you already ran on your own Sumsub account instead of verifying the user again. Post a single-use share token; Hodle forwards it to the provider, which reads the applicant back and opens a KYC attempt from it. A `202` means the token was accepted for processing, not that the user is approved \u2014 the applicant is validated asynchronously, so poll `GET /api/kyc/{attemptId}` or wait for the `kyc.completed` webhook. One token per user, and each token can only be imported once.",
        "tags": ["KYC"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/KycImportTokenRequest" }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Token accepted for processing. Poll `GET /api/kyc/{attemptId}` for the result.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/KycImportTokenResponse" }
              }
            }
          },
          "400": { "description": "Missing token, or a token the provider refused (`errorCode`: `INVALID_IMPORT_TOKEN`, `IMPORT_FAILED`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthorized." },
          "404": { "description": "Subaccount not found for this platform." },
          "409": { "description": "Token already consumed, or an import for this subaccount is still running (`errorCode`: `IMPORT_TOKEN_ALREADY_USED`, `IMPORT_IN_PROGRESS`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "502": { "description": "Sumsub import is not enabled for this account (`errorCode`: `IMPORT_NOT_ENABLED`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/account/statement": {
      "get": {
        "operationId": "accountStatement",
        "summary": "Account statement",
        "description": "Returns balances per asset and a paginated list of operations. Requires full KYC.",
        "tags": ["Account"],
        "parameters": [
          { "name": "from", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "ISO date/datetime. Defaults to 30 days ago." },
          { "name": "to", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "ISO date/datetime. Defaults to now. Window must be at most 90 days." },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "maximum": 200, "default": 50 }, "description": "Max operations per page." },
          { "name": "cursor", "in": "query", "schema": { "type": "string" }, "description": "Opaque cursor from a previous response's nextCursor." },
          { "name": "type", "in": "query", "schema": { "type": "array", "items": { "type": "string", "enum": ["deposit", "withdraw", "payout", "transfer"] } }, "description": "Filter by operation type. Repeat the param to pass multiple." }
        ],
        "responses": {
          "200": {
            "description": "Statement.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AccountStatementResponse" }
              }
            }
          },
          "403": { "description": "Full KYC required.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/wallet": {
      "get": {
        "operationId": "walletGet",
        "summary": "Read a wallet",
        "description": "Returns the addresses per network and current balances of the API-key user, or of one subaccount when `subAccountId` is given. `POST /api/wallet/get` serves the same payload with the fields in the body.",
        "tags": [
          "Wallet"
        ],
        "parameters": [
          {
            "name": "subAccountId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Read this subaccount's wallet instead of the API-key user's."
          },
          {
            "name": "walletId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            },
            "description": "Exact wallet ID scoped by owner and subAccountId. Unknown or foreign IDs return 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Wallet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletGetResponse"
                }
              }
            }
          },
          "404": {
            "description": "Wallet not provisioned."
          }
        }
      }
    },
    "/api/wallet/keys": {
      "post": {
        "tags": [
          "wallet"
        ],
        "summary": "Get the user's protected symmetric key",
        "description": "Returns the wallet's `protectedSymmetricKey` and the user's `email`. These\nvalues are required to derive the `symmetricKey` client-side before\ncalling `/api/wallet/payout`.\n\n**Requires the `WALLET_PAYOUT_API` per-user feature flag.** Without it\nthe endpoint responds `403`.\n\nPass `subAccountId` to read a customer sub-account's wallet instead of\nyour own. Without it the endpoint answers for the API key owner. The\n`email` returned is always the one that derives the key for the wallet\nbeing read, so use exactly the pair this endpoint returns together.\n\n**Aggressive rate limit: 1 request per minute**, counted per wallet — your\nown and each sub-account get their own window. Cache the response in your\ndatabase — these values only change if the user resets their PIN.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "walletId",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{24}$"
            },
            "description": "Exact wallet ID from wallet/create, scoped by owner and subAccountId. Also accepted in the JSON body. No fallback on an invalid reference."
          },
          {
            "in": "query",
            "name": "subAccountId",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Sub-account whose wallet key is returned. Must be a sub-account your\nAPI key created; any other id answers `404`. May also be sent in the\nJSON body.\n",
            "example": "be1424d8-0000-0000-0000-000000000000"
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the protected symmetric key and email",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "walletId": {
                          "type": "string",
                          "description": "Selected wallet ID. Cache the protected key per walletId."
                        },
                        "protectedSymmetricKey": {
                          "type": "string",
                          "description": "NIP-44 encrypted symmetric key (base64). Decrypt with PIN-derived masterKey.",
                          "example": "AoofiKHyVRLvdrknnXzo..."
                        },
                        "email": {
                          "type": "string",
                          "description": "Email that salts the Argon2id masterKey derivation for this wallet.",
                          "example": "user@example.com"
                        },
                        "subAccountId": {
                          "type": "string",
                          "nullable": true,
                          "description": "Sub-account this key belongs to, or `null` for your own wallet.",
                          "example": "be1424d8-0000-0000-0000-000000000000"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized (missing or invalid API key)"
          },
          "403": {
            "description": "User does not have the `WALLET_PAYOUT_API` feature flag enabled"
          },
          "404": {
            "description": "Wallet not found or has no protected symmetric key"
          },
          "429": {
            "description": "Too many requests",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until next allowed request"
              }
            }
          }
        }
      }
    },
    "/api/wallet/transfer": {
      "post": {
        "tags": [
          "wallet"
        ],
        "summary": "Transfer a stablecoin on Polygon, Base or Solana to an internal or external address",
        "description": "Transfers a token from the authenticated user's wallet. On Polygon and\nBase the server signs and submits an ERC-4337 UserOp (gas sponsored) from\nthe user's smart account. On Solana the server signs an SPL\n`transferChecked` with the user's Solana keypair and Hodler's master\nwallet pays the network fee (and the rent of the recipient's associated\ntoken account when it does not exist yet).\n\nThe recipient can be:\n  - an **external address** via `recipientAddress` — a checksummed EVM\n    address on `polygon`/`base`, a base58 address on `solana`. The\n    destination does not need to be a Hodler wallet.\n  - an **internal linked user** via `toSubAccountId` or `toUserId`\n    (must be linked to the same platform account). The recipient's\n    address is resolved on the same network as the request.\n\nRequires `walletPin` + `protectedSymmetricKey` (fetch the latter via\n`POST /api/wallet/keys`).\n\n**Requires the `WALLET_PAYOUT_API` per-user feature flag.** Without it\nthe endpoint responds `403`.\n\n**Asset selection:** the optional `asset` field defaults to `USDT` and\nthe optional `network` field defaults to `polygon`. Supported pairs:\n\n| network  | assets              |\n|----------|---------------------|\n| polygon  | USDT, USDC, BRLA    |\n| base     | USDC, BRLA          |\n| solana   | USDT, USDC, BRS     |\n\nOnly `USDT` on Polygon debits the internal USDT ledger (balance is\nchecked server-side). Every other pair is enforced on-chain — the\ntransaction fails if the wallet lacks funds.\n\n**BRS requires the `NORA_RAIL` per-user feature flag** (the Nora\nprovider). Without it the endpoint responds `403`.\n\nSolana is not available in sandbox — sandbox settles on Base Sepolia.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "amount",
                  "walletPin",
                  "protectedSymmetricKey"
                ],
                "properties": {
                  "recipientAddress": {
                    "type": "string",
                    "description": "Destination address (external or internal). Mutually\nexclusive with `toSubAccountId` / `toUserId`. Must be a valid\nchecksummed EVM address on `polygon`/`base`, or a valid\nbase58 address on `solana`.\n",
                    "example": "0x520ec4aD3BdC629D13a49dB558D7F6813f3696aD"
                  },
                  "toSubAccountId": {
                    "type": "string",
                    "description": "Sub-account id of a linked internal recipient. Resolves to\nthat user's wallet address.\n"
                  },
                  "toUserId": {
                    "type": "string",
                    "description": "User id of a linked internal recipient (must share the same\nlinked platform account).\n"
                  },
                  "fromSubAccountId": {
                    "type": "string",
                    "description": "Optional source sub-account. When omitted the transfer debits\nthe authenticated application user's own wallet.\n"
                  },
                  "amount": {
                    "type": "string",
                    "description": "Token amount to send. Min above 0, max 10000.",
                    "example": "50"
                  },
                  "network": {
                    "type": "string",
                    "enum": [
                      "polygon",
                      "base",
                      "solana"
                    ],
                    "default": "polygon",
                    "description": "Network the transfer runs on.",
                    "example": "solana"
                  },
                  "asset": {
                    "type": "string",
                    "enum": [
                      "USDT",
                      "USDC",
                      "BRLA",
                      "BRS"
                    ],
                    "default": "USDT",
                    "description": "Token to transfer. Defaults to `USDT`. Only `USDT` on Polygon\ntouches the internal ledger; every other pair relies on the\non-chain balance. `BRS` is Solana-only and requires the\n`NORA_RAIL` flag.\n",
                    "example": "BRS"
                  },
                  "reference": {
                    "type": "string",
                    "description": "Optional idempotency key. A non-failed transfer with the same\nreference within 2 minutes is returned as `alreadyProcessed`.\n",
                    "example": "order-9f3c1a"
                  },
                  "walletId": {
                    "type": "string",
                    "pattern": "^[a-fA-F0-9]{24}$",
                    "description": "ID returned by wallet/create. Selects the exact spending wallet within the source owner/subaccount scope, with no fallback. Use its protectedSymmetricKey. Sandbox supports explicit selection of PIN-protected Base Sepolia wallets. For signature-based payouts, use only on the prepare request; payoutIntentId selects the wallet on submission."
                  },
                  "walletPin": {
                    "type": "string",
                    "description": "User's wallet PIN. Combined with email + protectedSymmetricKey\nto derive the symmetricKey server-side via Argon2id.\n",
                    "example": "1234"
                  },
                  "protectedSymmetricKey": {
                    "type": "string",
                    "description": "NIP-44 encrypted symmetric key. Get from `POST /api/wallet/keys`.",
                    "example": "AoofiKHyVRLvdrknnXzo..."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transfer submitted and confirmed on-chain.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "txHash": {
                          "type": "string",
                          "example": "0xd3c1b3c6de23381ffa51ceae0712190a9406b939e5a33e2c25af0a37cb599be1"
                        },
                        "senderAddress": {
                          "type": "string",
                          "example": "0x369C5850F85ceAe372a34AD3Df1f6568289ec0D1"
                        },
                        "recipientAddress": {
                          "type": "string",
                          "example": "0x520ec4aD3BdC629D13a49dB558D7F6813f3696aD"
                        },
                        "amount": {
                          "type": "string",
                          "example": "50"
                        },
                        "asset": {
                          "type": "string",
                          "enum": [
                            "USDT",
                            "USDC",
                            "BRLA",
                            "BRS"
                          ],
                          "example": "BRS"
                        },
                        "transactionId": {
                          "type": "string",
                          "example": "65f1a8..."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failure, invalid recipient address, invalid PIN, or\ninsufficient USDT ledger balance (USDT only).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "error": {
                      "type": "string",
                      "example": "Invalid recipient address"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "The source account is blocked, the user does not have the\n`WALLET_PAYOUT_API` feature flag enabled, or the requested asset needs\na provider flag the account lacks (`BRS` needs `NORA_RAIL`).\n"
          },
          "429": {
            "description": "Rate limit hit (30s/user)."
          }
        }
      }
    },
    "/api/wallet/payout": {
      "post": {
        "tags": [
          "wallet"
        ],
        "summary": "Pay PIX from a wallet balance on Polygon, Base, Tron or Solana",
        "description": "Initiates a wallet → PIX payout. The server signs and submits\nthe on-chain UserOp on behalf of the user, then triggers PIX cashout\nin the background.\n\n**Returns immediately (HTTP 202).** Poll `GET /api/wallet/payout/{transactionId}`\nto track status.\n\nA request retried with the same `externalId` returns HTTP 200 with\n`alreadyProcessed: true` and the original `transactionId`.\n\nRequires `walletPin` + `protectedSymmetricKey` (fetch the latter via\n`POST /api/wallet/keys`).\n\n**Requires the `WALLET_PAYOUT_API` per-user feature flag.** Without it\nthe endpoint responds `403`.\n\nSupported network/asset pairs (`asset` is optional and uses the ordered\npreference shown below):\n\n| network  | assets            | automatic order       | debit destination            |\n|----------|-------------------|-----------------------|------------------------------|\n| polygon  | USDT, USDC, BRLA  | BRLA, then USDT, USDC | Avenia sub-account or master |\n| base     | USDC, BRLA        | BRLA, then USDC        | Avenia sub-account or master |\n| tron     | USDT (forced)     | USDT                  | Avenia Tron deposit address  |\n| solana   | USDT, USDC, BRS   | USDT, then USDC        | Hodler master Solana wallet  |\n\n**BRLA is tried first because direct USD-stablecoin liquidation has a\nhigher provider cost.** If BRLA cannot cover the complete debit — payout\nvalue plus the fee for BRLA — the server tries the next supported asset\ndirectly. It never performs USDC → BRLA → PIX as an intermediate swap.\nThere is no partial BRLA + USDC debit: one asset must cover the complete\namount. An explicit `asset: BRLA` has the same preference behavior. On\nPolygon/Base, an explicit USDC/USDT request selects that asset as the\ndirect fallback; on Solana, where BRLA is unavailable, the requested\nUSDC/USDT/BRS is debited directly. Fallback happens only when the balance\ncheck fails before signing; once a payout is prepared or processing, no\nsecond asset is attempted.\n\nOn Solana the server signs an SPL `transferChecked` with the user's Solana\nkeypair and Hodler's master wallet pays the network fee. Avenia has no\nSolana deposit address, so the debit lands on Hodler's master Solana\nwallet and the PIX leg is settled from the float.\n\n**BRS requires the `NORA_RAIL` per-user feature flag** (the Nora\nprovider); without it the endpoint responds `403`. BRS is BRL-pegged, so\nit is debited 1:1 with the BRL amount instead of going through the FX\nrate, and the PIX leg draws on the BRLA float.\n\n**Tron requires the `TRON_PAYOUT` per-user feature flag.**\n\nSolana is not available in sandbox — sandbox settles on Base Sepolia.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "value",
                  "network",
                  "walletPin",
                  "protectedSymmetricKey"
                ],
                "properties": {
                  "value": {
                    "type": "integer",
                    "description": "Amount in BRL cents. Min 10. Max is the user per-tx limit (`perTxLimitBrl`, default R$ 3000).",
                    "example": 5000
                  },
                  "network": {
                    "type": "string",
                    "enum": [
                      "polygon",
                      "base",
                      "tron",
                      "solana"
                    ],
                    "description": "Source network the stablecoin debit runs on.",
                    "example": "solana"
                  },
                  "asset": {
                    "type": "string",
                    "enum": [
                      "USDT",
                      "USDC",
                      "BRLA",
                      "BRS"
                    ],
                    "description": "Stablecoin preference to debit. Optional — tries the automatic\norder shown in the table above. `BRLA` may fall back to the\nnext supported direct asset when the full debit cannot fit.\nOn Polygon/Base, explicit USDC/USDT is the direct fallback;\non Solana they are debited directly because BRLA is not\nsupported. Ignored on `tron`, which always debits USDT.\n`BRS` is Solana-only and requires the `NORA_RAIL` flag.\n",
                    "example": "BRS"
                  },
                  "pixKey": {
                    "type": "string",
                    "description": "PIX key of the recipient. Mutually exclusive with qrCode.",
                    "example": "recipient@example.com"
                  },
                  "pixKeyType": {
                    "type": "string",
                    "enum": [
                      "PHONE",
                      "CPF",
                      "EMAIL",
                      "RANDOM",
                      "CNPJ"
                    ],
                    "description": "Required when pixKey is provided.",
                    "example": "EMAIL"
                  },
                  "qrCode": {
                    "type": "string",
                    "description": "PIX BR Code copy-paste. Mutually exclusive with pixKey."
                  },
                  "walletId": {
                    "type": "string",
                    "pattern": "^[a-fA-F0-9]{24}$",
                    "description": "ID returned by wallet/create. Selects the exact spending wallet within the source owner/subaccount scope, with no fallback. Use its protectedSymmetricKey. Sandbox supports explicit selection of PIN-protected Base Sepolia wallets. For signature-based payouts, use only on the prepare request; payoutIntentId selects the wallet on submission."
                  },
                  "walletPin": {
                    "type": "string",
                    "description": "User's wallet PIN. Combined with email + protectedSymmetricKey\nto derive the symmetricKey server-side via Argon2id.\n",
                    "example": "1234"
                  },
                  "protectedSymmetricKey": {
                    "type": "string",
                    "description": "NIP-44 encrypted symmetric key. Get from `POST /api/wallet/keys`.",
                    "example": "AoofiKHyVRLvdrknnXzo..."
                  },
                  "externalId": {
                    "type": "string",
                    "description": "Optional caller-provided idempotency key. A repeated request\nwith the same externalId returns the original payout as\n`alreadyProcessed`, even after the heuristic window expires.\nDifferent externalIds are treated as separate payout orders\nand bypass the short-window duplicate heuristic. Without an\nexternalId, requests with the same user, value and PIX key\nmay be deduplicated heuristically for a short window.\n",
                    "example": "offramp:order-9f3c1a"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing payout returned for an idempotent retry.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "alreadyProcessed",
                    "transactionId",
                    "status"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "alreadyProcessed": {
                      "type": "boolean",
                      "example": true
                    },
                    "transactionId": {
                      "type": "string",
                      "example": "65f1a8..."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "PENDING",
                        "COMPLETED",
                        "FAILED",
                        "REFUNDED"
                      ]
                    },
                    "txHash": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Payout queued. Poll the GET endpoint for status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "transactionId": {
                      "type": "string",
                      "example": "65f1a8..."
                    },
                    "externalId": {
                      "type": "string",
                      "nullable": true,
                      "description": "The idempotency key sent in the request, echoed back.",
                      "example": "offramp:823e6356"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "PROCESSING"
                      ]
                    },
                    "stableAmount": {
                      "type": "string",
                      "description": "USDT/USDC amount that will be transferred.",
                      "example": "9.31"
                    },
                    "valueInBrl": {
                      "type": "string",
                      "example": "50.00"
                    },
                    "fee": {
                      "type": "string",
                      "example": "1.55"
                    },
                    "network": {
                      "type": "string",
                      "example": "solana"
                    },
                    "asset": {
                      "type": "string",
                      "enum": [
                        "USDT",
                        "USDC",
                        "BRLA",
                        "BRS"
                      ],
                      "example": "BRS"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failure, invalid PIN, or insufficient balance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "error": {
                      "type": "string",
                      "example": "Invalid PIN"
                    },
                    "errorCode": {
                      "type": "string",
                      "enum": [
                        "INVALID_PIN",
                        "RATE_LIMITED",
                        "WALLET_NOT_FOUND",
                        "PREPARE_FAILED",
                        "INSUFFICIENT_BALANCE"
                      ],
                      "example": "INSUFFICIENT_BALANCE"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "The user account is blocked, the user does not have the\n`WALLET_PAYOUT_API` feature flag enabled, a per-tx/total limit was\nexceeded, or the requested asset needs a provider flag the account\nlacks (`BRS` needs `NORA_RAIL`).\n"
          },
          "429": {
            "description": "Rate limit hit (30s/user) or 3 invalid PIN attempts → 24h lock"
          },
          "503": {
            "description": "PIX payments globally disabled (feature flag)"
          }
        }
      }
    },
    "/api/wallet/payout/{transactionId}": {
      "get": {
        "operationId": "walletPayoutStatus",
        "summary": "Get payout status",
        "tags": ["Wallet"],
        "parameters": [
          { "name": "transactionId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Status.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WalletPayoutStatusResponse" }
              }
            }
          }
        }
      }
    },
    "/api/deposit/asset": {
      "post": {
        "operationId": "depositAsset",
        "summary": "Create a deposit (PIX → crypto)",
        "description": "Convert BRL to Lightning, USDT, USDC, or USDCE and deliver to an address. Requires full KYC.",
        "tags": ["Deposits"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/DepositAssetRequest" },
              "examples": {
                "lightning": {
                  "summary": "Lightning deposit",
                  "value": { "value": 5000, "address": "lnbc500u1pj...", "asset": "LIGHTNING", "externalId": "order-001" }
                },
                "usdt": {
                  "summary": "USDT (Polygon) deposit",
                  "value": { "value": 25000, "asset": "USDT", "network": "polygon", "address": "0x9aB7a1..." }
                },
                "usdc": {
                  "summary": "USDC (Base) deposit",
                  "value": { "value": 25000, "asset": "USDC", "network": "base", "address": "0x9aB7a1..." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deposit created.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DepositAssetResponse" }
              }
            }
          },
          "400": { "description": "Validation error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Full KYC required.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Duplicate `externalId`." }
        }
      }
    },
    "/api/deposit/asset/{externalId}": {
      "get": {
        "operationId": "depositAssetStatus",
        "summary": "Get deposit status and payer",
        "description": "Read a deposit back: whether the PIX was paid, whether the asset was delivered, and who paid the QR code. The `payer` block is only returned to accounts that own a virtual account.",
        "tags": ["Deposits"],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The `externalId` of the deposit — your own value, or the UUID returned by the POST.",
            "schema": { "type": "string" }
          },
          {
            "name": "subAccountId",
            "in": "query",
            "required": false,
            "description": "Required when the deposit was created with `subAccountId`. An id you do not own is refused with 404.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Current state of the deposit.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DepositAssetStatusResponse" }
              }
            }
          },
          "401": { "description": "Unauthorized.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Deposit not found, or it belongs to another API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/lightning/invoice": {
      "post": {
        "operationId": "createLightningInvoice",
        "summary": "Create a Lightning invoice (Lightning → PIX)",
        "description": "Generate a Lightning invoice that, once paid, triggers an automatic PIX payout.",
        "tags": ["Lightning"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/LightningInvoiceRequest" },
              "examples": {
                "withPixKey": {
                  "summary": "With PIX key",
                  "value": { "value": 1000, "pixKey": "user@email.com", "pixKeyType": "EMAIL" }
                },
                "withQrCode": {
                  "summary": "With PIX BR Code",
                  "value": { "value": 1000, "qrCode": "00020126580014br.gov.bcb.pix..." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invoice created.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LightningInvoiceResponse" }
              }
            }
          },
          "400": { "description": "Validation error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/webhook/{webhookId}": {
      "post": {
        "operationId": "webhookDelivery",
        "summary": "Webhook delivery (your server receives this)",
        "description": "Hodle POSTs to your endpoint with `event` + `data`. Verify the `X-Hodle-Signature` header (HMAC-SHA256 of `${timestamp}.${rawBody}`).",
        "tags": ["Webhooks"],
        "parameters": [
          { "name": "webhookId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "X-Hodle-Signature", "in": "header", "required": true, "schema": { "type": "string" }, "description": "HMAC-SHA256 of `${timestamp}.${payload}` using your webhook secret." },
          { "name": "X-Hodle-Timestamp", "in": "header", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WebhookPayload" }
            }
          }
        },
        "responses": {
          "200": { "description": "Acknowledged. 2xx is required to mark delivery as successful." }
        }
      }
    },
    "/api/wallet/create": {
      "post": {
        "tags": [
          "wallet"
        ],
        "summary": "Create a PIN-protected wallet",
        "description": "Requires the six-digit walletPin established on the platform. The PIN is\nverified against an existing protected wallet owned by the target user.\nEach successful call creates fresh wallet keys protected by that same PIN.\nMissing PIN never creates a wallet. Save data.id as walletId and use the\nnew protectedSymmetricKey for reads, transfers and payouts of that wallet.\nThree invalid PIN attempts lock creation for 24 hours.\nSandbox can bootstrap the first protected EVM wallet with the supplied PIN\nand uses Base Sepolia (chainId 84532). Solana is unavailable in sandbox.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "walletPin"
                ],
                "properties": {
                  "walletPin": {
                    "type": "string",
                    "pattern": "^\\d{6}$",
                    "description": "The platform wallet PIN, verified before creation."
                  },
                  "protectedSymmetricKey": {
                    "type": "string",
                    "description": "Optional existing owner wallet key used as the PIN reference. The response contains a fresh protected key for the new wallet."
                  },
                  "network": {
                    "type": "string",
                    "enum": [
                      "polygon",
                      "base",
                      "solana"
                    ],
                    "default": "polygon"
                  },
                  "subAccountId": {
                    "type": "string",
                    "description": "Owned customer subaccount containing the new wallet."
                  },
                  "userId": {
                    "type": "string",
                    "description": "Linked target user. Omit when using subAccountId."
                  },
                  "default": {
                    "type": "boolean",
                    "description": "Make this wallet default within its scope. Defaults to true only for the first wallet in that scope."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created a wallet with PIN-protected keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Pass this as walletId in subsequent requests."
                        },
                        "address": {
                          "type": "string"
                        },
                        "network": {
                          "type": "string"
                        },
                        "chainId": {
                          "type": "integer",
                          "description": "EVM chain ID; 84532 in sandbox. Omitted for Solana."
                        },
                        "balance": {
                          "type": "number"
                        },
                        "protectedSymmetricKey": {
                          "type": "string",
                          "description": "The new wallet's PIN-protected symmetric key."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request, missing platform PIN, invalid reference key, or unsupported network"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Incorrect platform wallet PIN"
          },
          "404": {
            "description": "Target user or owned subaccount not found"
          },
          "429": {
            "description": "PIN attempts locked for 24 hours"
          }
        }
      }
    },
    "/api/wallet/get": {
      "post": {
        "operationId": "walletGetPost",
        "summary": "Read a wallet by ID or default network selection",
        "description": "Returns the addresses per network and current balances of the API-key user, or of one subaccount when `subAccountId` is given. `POST /api/wallet/get` serves the same payload with the fields in the body.",
        "tags": [
          "Wallet"
        ],
        "responses": {
          "200": {
            "description": "Wallet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletGetResponse"
                }
              }
            }
          },
          "404": {
            "description": "Wallet not provisioned."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "walletId": {
                    "type": "string",
                    "pattern": "^[a-fA-F0-9]{24}$",
                    "description": "ID returned by wallet/create. Scoped by the authenticated owner and subAccountId; no fallback for missing or foreign wallets."
                  },
                  "subAccountId": {
                    "type": "string",
                    "description": "Owned subaccount containing the wallet."
                  },
                  "sync": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "tags": [
    { "name": "Subaccounts", "description": "Create and read end-user subaccounts." },
    { "name": "KYC", "description": "Submit and inspect KYC for end-users." },
    { "name": "Account", "description": "Statement and balances." },
    { "name": "Pricing", "description": "Indicative quotes for BRL ↔ asset pairs." },
    { "name": "Wallet", "description": "Read wallets, fetch keys, transfer USDT, run PIX payouts." },
    { "name": "Deposits", "description": "PIX → crypto on-ramp." },
    { "name": "Lightning", "description": "Lightning ↔ PIX off-ramp." },
    { "name": "Webhooks", "description": "Realtime event delivery." }
  ]
}
