{
  "openapi": "3.0.3",
  "info": {
    "title": "HotVoucher Merchant API",
    "version": "1.0",
    "description": "Server-to-server API for merchants: accept payments (HotVoucher Pay), redeem and issue HotVoucher vouchers, give your customers permanent deposit addresses, send payouts, convert between your own coins, and read prices and coin status.\n\nAuthentication: every request carries your API token in the header  Authorization: Bearer hvk_YOUR_TOKEN . A token sent in the URL or in the body is refused. Requests are accepted only from the IP addresses whitelisted for the token, so call the API from your server - never from a browser or a mobile app, and never put the token in client-side code.\n\nPermissions: each token works only in the sections ticked for it in the HotVoucher app (shown as \"Permission\" on every endpoint). A call outside them answers 403 SECTION_NOT_ALLOWED. The payout and swap sections are off by default. GET /me lists the sections of your token.\n\nAll responses are JSON. Errors have the form {error, code}.\n\nIdempotency: POST /payouts and POST /swap need an idempotencyKey. Sending the same key again returns the same result (replay: true) and never moves money twice; the same key with a different body answers 409 IDEMPOTENCY_MISMATCH. A rejected request (400) moves nothing and frees the key.\n\nRate limits per token (standard tier): 600 requests per minute; 500 new customer addresses per day; voucher create 600 per hour; voucher redeem 300 per 10 minutes; payouts and swaps 300 per minute. Over the limit: 429 RATE_LIMIT. Higher tiers on request.\n\nWebhooks: set a callback URL on the token and every event is sent to it as a signed HTTPS POST - see the Webhooks section. Verify X-HotVoucher-Signature on every message.\n\nThe full plain-text guide is at GET /docs.",
    "x-logo": { "url": "../assets/hotvoucher-logo.png" }
  },
  "servers": [
    { "url": "https://hotvoucher.com/api/v1", "description": "Production" }
  ],
  "security": [ { "bearerAuth": [] } ],
  "tags": [
    { "name": "Account", "description": "Merchant and token info, balances, transactions and a ledger statement for reconciliation. Permission: inquiry." },
    { "name": "Vouchers", "description": "Look up, redeem, issue and cancel HotVoucher voucher codes (USD-XXXX-XXXX-XXXX-XXXX-XXXX; codes made before v965 start with HTV- and stay valid, the prefix is optional). POST /vouchers/inquiry also answers for Utopia uVouchers (same code format), checked on the Utopia network through a queue. Permissions: code_inquiry, code_redeem, code_issue." },
    { "name": "Payment gateway", "description": "HotVoucher Pay: your server creates an invoice, the buyer pays on the HotVoucher payment page (wallet, voucher code or crypto). Whatever the buyer pays is credited to your account in the same coin (minus fees) unless you set one automatic conversion coin in the app. Confirm a payment with the webhook or GET /invoices/{id}, never from the page. A crypto payment is credited only after the coin's own explorer confirms it. Permission: merchant." },
    { "name": "Customer addresses", "description": "Every customer gets one permanent deposit address per network, free of charge. Any amount that arrives on it is yours; credit your customer on your side. Permission: address." },
    { "name": "Payouts", "description": "Send coins from your HotVoucher balance to any address, for example when a customer of your site withdraws. Same pipeline as a withdrawal in the app: network fee, limits, admin review above your auto-approve limit. Permission: payout (OFF by default)." },
    { "name": "Coins and prices", "description": "Which coins and networks are active right now, network fees and minimums, and live prices. Permission: inquiry." },
    { "name": "Swap", "description": "Convert part of your balance from one coin to another at HotVoucher's live rate (the same rates and fees as the app). Nothing leaves HotVoucher. Permission: swap (OFF by default)." },
    { "name": "Webhooks", "description": "Send a test callback and read the delivery log of your token. Permission: inquiry." },
    { "name": "Guide", "description": "The plain-text guide." }
  ],
  "paths": {
    "/me": {
      "get": {
        "tags": ["Account"], "operationId": "getMe", "x-section": "inquiry",
        "summary": "Merchant and token info",
        "description": "Your merchant id and name, and the token's own settings: sections (permissions), whitelisted IPs, cashback, site URL and callback state.",
        "responses": {
          "200": { "description": "Merchant and token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Me" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/balance": {
      "get": {
        "tags": ["Account"], "operationId": "getBalance", "x-section": "inquiry",
        "summary": "Balance in every coin",
        "responses": {
          "200": { "description": "Balances by coin symbol", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Balances" }, "example": { "balances": { "HTV": 1250.5, "USDT": 310.25, "BTC": 0.0021 } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/transactions": {
      "get": {
        "tags": ["Account"], "operationId": "listTransactions", "x-section": "inquiry",
        "summary": "Latest transactions of the merchant account",
        "parameters": [ { "name": "limit", "in": "query", "description": "How many (1-200, default 50).", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } } ],
        "responses": {
          "200": { "description": "Newest first", "content": { "application/json": { "schema": { "type": "object", "properties": { "tx": { "type": "array", "items": { "$ref": "#/components/schemas/Transaction" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/statement": {
      "get": {
        "tags": ["Account"], "operationId": "getStatement", "x-section": "inquiry",
        "summary": "Ledger statement per coin for a period",
        "description": "Use it to reconcile your own book against HotVoucher. opening = closing - net; closing = your balance at \"to\" (the live balance when \"to\" is now). Types in byType: pay_in (gateway payments credited), pay_fee, withdraw (payouts, API or app), swap, voucher, api_redeem, internal_in, internal_out, deposit, addr_fee.",
        "parameters": [
          { "name": "from", "in": "query", "description": "Start, Unix time in milliseconds (default: 30 days ago).", "schema": { "type": "integer", "format": "int64" } },
          { "name": "to", "in": "query", "description": "End, Unix time in milliseconds (default: now).", "schema": { "type": "integer", "format": "int64" } },
          { "name": "coin", "in": "query", "description": "Only this coin (symbol).", "schema": { "type": "string", "example": "USDT" } }
        ],
        "responses": {
          "200": { "description": "Statement", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Statement" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/check": {
      "post": {
        "tags": ["Vouchers"], "operationId": "checkVoucher", "x-section": "code_inquiry",
        "summary": "Status of a voucher, without redeeming it",
        "description": "Send the code or the ref. For a voucher you created yourself the answer also names the parties (account ids) and the dates.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheckRequest" }, "example": { "code": "USD-XXXX-XXXX-XXXX-XXXX-XXXX" } } } },
        "responses": {
          "200": { "description": "found:false when no voucher matches", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VoucherCheck" }, "example": { "found": true, "status": "active", "amount": 25, "coin": "HTV", "ref": "VC-8F3K2M9Q", "locked": false } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/redeem": {
      "post": {
        "tags": ["Vouchers"], "operationId": "redeemVoucher", "x-section": "code_redeem",
        "summary": "Redeem a customer's voucher in favour of the merchant",
        "description": "The voucher amount is credited to your balance. Cashback: the percentage configured on the token is taken from your balance at that moment and credited to the account that created the voucher (the customer). Official HotVoucher vouchers (no creator) carry no cashback. Webhook event: redeem.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RedeemRequest" }, "example": { "code": "USD-XXXX-XXXX-XXXX-XXXX-XXXX", "reference": "order-123" } } } },
        "responses": {
          "200": { "description": "Redeemed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RedeemResult" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }, "409": { "$ref": "#/components/responses/Conflict" }, "410": { "$ref": "#/components/responses/Gone" },
          "429": { "$ref": "#/components/responses/RateLimited" }, "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/voucher/create": {
      "post": {
        "tags": ["Vouchers"], "operationId": "createVoucher", "x-section": "code_issue",
        "summary": "Create a voucher from your balance",
        "description": "For rewards or refunds. The amount is taken from your HTV balance (at least 0.1 HTV). With lockTo (the customer's HV-... id) only that account can redeem it and the code is delivered to their notifications. Webhook event: voucher_created.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VoucherCreateRequest" }, "example": { "amount": 25, "lockTo": "HV-ABC123", "reference": "refund-77" } } } },
        "responses": {
          "200": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VoucherCreated" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "423": { "$ref": "#/components/responses/Locked" }, "429": { "$ref": "#/components/responses/RateLimited" }, "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/voucher/cancel": {
      "post": {
        "tags": ["Vouchers"], "operationId": "cancelVoucher", "x-section": "code_issue",
        "summary": "Cancel an unredeemed voucher you created",
        "description": "The amount returns to your balance. Webhook event: voucher_cancelled.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VoucherCancelRequest" }, "example": { "ref": "VC-8F3K2M9Q" } } } },
        "responses": {
          "200": { "description": "Cancelled", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" } }, "additionalProperties": true } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }, "409": { "$ref": "#/components/responses/Conflict" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/vouchers": {
      "get": {
        "tags": ["Vouchers"], "operationId": "listVouchers", "x-section": "code_inquiry",
        "summary": "Vouchers you created (with their codes)",
        "parameters": [
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["all", "active", "redeemed", "void"], "default": "all" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "minimum": 0, "default": 0 } }
        ],
        "responses": {
          "200": { "description": "Newest first; more = another page exists", "content": { "application/json": { "schema": { "type": "object", "properties": { "vouchers": { "type": "array", "items": { "$ref": "#/components/schemas/Voucher" } }, "limit": { "type": "integer" }, "offset": { "type": "integer" }, "more": { "type": "boolean" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/vouchers/inquiry": {
      "post": {
        "tags": ["Vouchers"], "operationId": "voucherInquiry", "x-section": "code_inquiry",
        "summary": "Voucher inquiry: a HotVoucher voucher or a Utopia uVoucher, without redeeming it",
        "description": "Both kinds of code look like USD-XXXX-XXXX-XXXX-XXXX-XXXX; say which it is with kind (optional; without it HotVoucher vouchers are looked up first). A HotVoucher voucher is answered at once from HotVoucher (the same fields as POST /check, kind hotvoucher). A uVoucher ALWAYS needs hash = the transaction that created it, and the Utopia network checks the code against that transaction (kind uvoucher without hash = 400 HASH_REQUIRED). Network inquiries wait in ONE queue for every caller (one on the network at a time), so the answer is 200 when it is ready within about 15 seconds, else 202 with an inquiryId: ask GET /vouchers/inquiry/{inquiryId} after retryAfter seconds. The same code + hash asked again is answered from memory (a redeemed uVoucher for 24 hours). Nothing moves; the parties are never named.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VoucherInquiryRequest" }, "examples": { "hotvoucher": { "summary": "A HotVoucher voucher", "value": { "kind": "hotvoucher", "code": "USD-XXXX-XXXX-XXXX-XXXX-XXXX" } }, "uvoucher": { "summary": "A uVoucher with the hash of the transaction that created it", "value": { "kind": "uvoucher", "code": "USD-XXXX-XXXX-XXXX-XXXX-XXXX", "hash": "8F2C4A0B9D3E7F1A6C5B2E9D0A4F7C3B" } } } } } },
        "responses": {
          "200": { "description": "The answer. found:false + needHash:true (sent without kind) = not a HotVoucher voucher; a uVoucher needs hash.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VoucherInquiry" }, "examples": { "hotvoucher": { "summary": "A HotVoucher voucher", "value": { "kind": "hotvoucher", "checkedBy": "hotvoucher", "found": true, "status": "active", "amount": 25, "coin": "HTV", "ref": "VC-8F3K2M9Q", "locked": false } }, "uvoucher": { "summary": "A uVoucher checked on the Utopia network", "value": { "kind": "uvoucher", "checkedBy": "utopia-network", "found": true, "status": "active", "amount": 50, "coin": "UUSD", "createdAt": 1794651600000, "inquiryId": "UVQ-3F9A1C7B2D4E" } }, "needHash": { "summary": "Unknown without the hash", "value": { "kind": "uvoucher", "found": false, "status": "unknown", "needHash": true } } } } } },
          "202": { "description": "The uVoucher waits in the inquiry queue or is being checked - ask GET /vouchers/inquiry/{inquiryId} after retryAfter seconds (also in the Retry-After header).", "headers": { "Retry-After": { "schema": { "type": "integer" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VoucherInquiryQueued" }, "example": { "kind": "uvoucher", "status": "queued", "inquiryId": "UVQ-3F9A1C7B2D4E", "position": 3, "retryAfter": 30 } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "description": "RATE_LIMIT (the token's limit) or QUEUE_LIMIT (you already have 5 inquiries waiting - ask again when they are answered)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "502": { "description": "VERIFY_FAILED - the network did not take the inquiry", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "description": "QUEUE_FULL (many inquiries are waiting - try again after the Retry-After header) or NO_NODE (the Utopia network is not available)", "headers": { "Retry-After": { "schema": { "type": "integer" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "504": { "description": "VERIFY_TIMEOUT - the network did not answer in time; try again in a minute", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/vouchers/inquiry/{id}": {
      "get": {
        "tags": ["Vouchers"], "operationId": "voucherInquiryResult", "x-section": "code_inquiry",
        "summary": "The answer of a queued uVoucher inquiry",
        "description": "Use the inquiryId of a 202 answer. 202 again while it waits (wait retryAfter seconds), 200 with the answer when it is ready. An inquiry is kept 30 minutes and only for the merchant that asked it; after that (or after a HotVoucher restart) send the inquiry again - a ready answer is kept in memory.",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "example": "UVQ-3F9A1C7B2D4E" } } ],
        "responses": {
          "200": { "description": "The answer (as POST /vouchers/inquiry)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VoucherInquiry" } } } },
          "202": { "description": "Still waiting or being checked", "headers": { "Retry-After": { "schema": { "type": "integer" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VoucherInquiryQueued" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "description": "VERIFY_FAILED - the network did not take the inquiry", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "504": { "description": "VERIFY_TIMEOUT - the network did not answer in time; send the inquiry again in a minute", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/invoices": {
      "post": {
        "tags": ["Payment gateway"], "operationId": "createInvoice", "x-section": "merchant",
        "summary": "Create an invoice",
        "description": "Send the buyer to payUrl (a full-page redirect; the page is not meant to be framed or opened by a script). Without amount = an open top-up invoice. Any amount that arrives - full, partial, more, or after the invoice expired - is yours. If an invoice has an amount and less arrives, the buyer can send the rest to the same address until it expires. Webhook events: payment.detected, payment.received, invoice.paid, invoice.expired.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InvoiceCreateRequest" }, "example": { "customer": "user-1042", "amount": 20, "currency": "USD", "orderId": "order-123", "returnUrl": "https://shop.example/thanks", "lang": "en" } } } },
        "responses": {
          "200": { "description": "Invoice (status pending) with payUrl", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Invoice" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }, "503": { "$ref": "#/components/responses/Unavailable" }
        }
      },
      "get": {
        "tags": ["Payment gateway"], "operationId": "listInvoices", "x-section": "merchant",
        "summary": "List invoices",
        "parameters": [
          { "name": "customer", "in": "query", "schema": { "type": "string", "maxLength": 64 } },
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["pending", "paid", "expired"] } },
          { "name": "limit", "in": "query", "description": "1-200, default 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } }
        ],
        "responses": {
          "200": { "description": "Newest first", "content": { "application/json": { "schema": { "type": "object", "properties": { "invoices": { "type": "array", "items": { "$ref": "#/components/schemas/Invoice" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/invoices/{id}": {
      "get": {
        "tags": ["Payment gateway"], "operationId": "getInvoice", "x-section": "merchant",
        "summary": "One invoice with its payments",
        "description": "detected = crypto payments seen on the network but not confirmed yet (verifying: true while the explorer check runs).",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": {
          "200": { "description": "Invoice", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InvoiceDetail" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/payments": {
      "get": {
        "tags": ["Payment gateway"], "operationId": "listPayments", "x-section": "merchant",
        "summary": "Every received payment (with or without an invoice)",
        "parameters": [
          { "name": "customer", "in": "query", "schema": { "type": "string", "maxLength": 64 } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } }
        ],
        "responses": {
          "200": { "description": "Newest first", "content": { "application/json": { "schema": { "type": "object", "properties": { "payments": { "type": "array", "items": { "$ref": "#/components/schemas/Payment" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/payments/pending": {
      "get": {
        "tags": ["Payment gateway"], "operationId": "listPendingPayments", "x-section": "merchant",
        "summary": "Crypto payments seen on the network, not confirmed yet",
        "description": "The \"processing\" step. watch=1 together with a customer = that customer is on your deposit page: their addresses are polled every 30 seconds for the next 3 minutes - call it while the page is open.",
        "parameters": [
          { "name": "customer", "in": "query", "schema": { "type": "string", "maxLength": 64 } },
          { "name": "watch", "in": "query", "schema": { "type": "integer", "enum": [1] } }
        ],
        "responses": {
          "200": { "description": "Pending payments", "content": { "application/json": { "schema": { "type": "object", "properties": { "pending": { "type": "array", "items": { "$ref": "#/components/schemas/PendingPayment" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/fees": {
      "get": {
        "tags": ["Payment gateway"], "operationId": "getFees", "x-section": "merchant",
        "summary": "Your gateway fee and the network fee of every coin and network",
        "description": "Fees: the gateway fee % on every payment, plus - for crypto payments only - the network fee of moving the coins to our hot wallet (networkFeeUsd per coin and network). Every payment.received webhook lists feeUsd and networkFeeUsd.",
        "responses": {
          "200": { "description": "Fees", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Fees" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/addresses": {
      "get": {
        "tags": ["Customer addresses"], "operationId": "listAddresses", "x-section": "address",
        "summary": "The deposit addresses created for your customers",
        "parameters": [
          { "name": "customer", "in": "query", "schema": { "type": "string", "maxLength": 64 } },
          { "name": "coin", "in": "query", "schema": { "type": "string" } },
          { "name": "network", "in": "query", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "minimum": 0, "default": 0 } }
        ],
        "responses": {
          "200": { "description": "Addresses", "content": { "application/json": { "schema": { "type": "object", "properties": { "addresses": { "type": "array", "items": { "$ref": "#/components/schemas/Address" } }, "total": { "type": "integer" }, "more": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/addresses/validate": {
      "get": {
        "tags": ["Customer addresses"], "operationId": "validateAddress", "x-section": "address",
        "summary": "Check an address format and whether it is one of your customers' addresses",
        "parameters": [
          { "name": "address", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "network", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "valid = the format is correct for that network; mine = it is one of YOUR customers' gateway addresses", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddressValidation" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/customers/address": {
      "post": {
        "tags": ["Customer addresses"], "operationId": "getCustomerAddress", "x-section": "address",
        "summary": "The customer's permanent deposit address for a coin and network",
        "description": "Created on the first call, the same address afterwards - show it in your own UI. Free of charge. Deposits arriving on it come as payment.received webhooks.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CustomerAddressRequest" }, "example": { "customer": "user-1042", "coin": "USDT", "network": "TRC20" } } } },
        "responses": {
          "200": { "description": "Address", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CustomerAddress" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }, "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/customers/uusd-voucher": {
      "post": {
        "tags": ["Customer addresses"], "operationId": "redeemUusdVoucher", "x-section": "address",
        "summary": "Redeem a UUSD uVoucher (Utopia network) for a customer",
        "description": "Credited like a deposit to the customer (payment.received). pending: true = the network has not shown the activation yet: a valid code arrives as payment.received, otherwise the webhook uvoucher.failed follows within about 10 minutes. A code that already went through HotVoucher is refused at once (400 VOUCHER_USED; 409 VOUCHER_BUSY while the same customer's first try is still open). A HotVoucher voucher code (they start with USD- too) is refused with 400 HOTVOUCHER_VOUCHER - redeem it with POST /redeem.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UusdVoucherRequest" }, "example": { "customer": "user-1042", "code": "USD-XXXX-XXXX-XXXX-XXXX-XXXX" } } } },
        "responses": {
          "200": { "description": "Accepted (credited, or pending)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UusdVoucherResult" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/Conflict" }, "429": { "$ref": "#/components/responses/RateLimited" }, "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/customers/uusd-verify": {
      "post": {
        "tags": ["Customer addresses"], "operationId": "verifyUusd", "x-section": "address",
        "summary": "The Utopia network's answer about a uVoucher or a transfer",
        "description": "hash + code = a uVoucher; hash alone = a transfer. At most 10 inquiries per 10 minutes per customer. Network inquiries share ONE queue with POST /vouchers/inquiry: when the answer is not ready within about 9 seconds you get 503 VERIFY_QUEUED - ask again after the Retry-After header and the answer will be waiting (a merchant's own uVoucher checks are simpler with POST /vouchers/inquiry).",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UusdVerifyRequest" } } } },
        "responses": {
          "200": { "description": "Network answer", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "kind": { "type": "string", "enum": ["voucher", "transaction"] }, "info": { "type": "object", "additionalProperties": true } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "description": "RATE / RATE_LIMIT, or QUEUE_LIMIT (this customer already has 5 inquiries waiting)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "502": { "description": "The network did not answer (VERIFY_FAILED)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "description": "VERIFY_QUEUED (in line - ask again after Retry-After, the answer will be ready), QUEUE_FULL or NO_NODE", "headers": { "Retry-After": { "schema": { "type": "integer" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "504": { "description": "VERIFY_TIMEOUT - the network did not answer in time", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/payouts": {
      "post": {
        "tags": ["Payouts"], "operationId": "createPayout", "x-section": "payout",
        "summary": "Send coins to an address",
        "description": "amount = what your customer asked for, in coin units; the network fee is always reported separately. feePayer \"customer\" (default): the address receives amount - fee and your balance is debited amount. feePayer \"merchant\": the address receives exactly amount and your balance is debited amount + fee. The fee is the real cost on that network at the moment the payout is created; it is 0 when the address belongs to a HotVoucher account (kind \"internal\"). dryRun: true runs every check and answers what WOULD happen, moving nothing (idempotencyKey optional then). A payout below the network minimum (quote.min) is refused. Status: approved (queued) | review (large amount, waits for HotVoucher) | sending (signed, txId set) | sent (confirmed, or instant for a HotVoucher address) | failed (failReason). Treat sent and failed as final. Webhook event: payout.status on every change.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PayoutRequest" }, "example": { "idempotencyKey": "wd-88213", "coin": "USDT", "network": "TRC20", "address": "TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "amount": 50, "feePayer": "customer", "customer": "user-1042", "reference": "withdrawal 88213" } } } },
        "responses": {
          "201": { "description": "Payout created", "content": { "application/json": { "schema": { "type": "object", "properties": { "payout": { "$ref": "#/components/schemas/Payout" } } } } } },
          "200": { "description": "Replay of an existing idempotencyKey (payout.replay: true), or a dry run ({dryRun: true, ok, expectedStatus, quote, amountUsd, limits})", "content": { "application/json": { "schema": { "oneOf": [ { "type": "object", "properties": { "payout": { "$ref": "#/components/schemas/Payout" } } }, { "$ref": "#/components/schemas/PayoutDryRun" } ] } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/Conflict" }, "423": { "$ref": "#/components/responses/Locked" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "get": {
        "tags": ["Payouts"], "operationId": "listPayouts", "x-section": "payout",
        "summary": "Your payouts, newest first",
        "parameters": [
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["approved", "review", "sending", "sent", "failed"] } },
          { "name": "customer", "in": "query", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1 } }
        ],
        "responses": {
          "200": { "description": "Payouts", "content": { "application/json": { "schema": { "type": "object", "properties": { "payouts": { "type": "array", "items": { "$ref": "#/components/schemas/Payout" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/payouts/{id}": {
      "get": {
        "tags": ["Payouts"], "operationId": "getPayout", "x-section": "payout",
        "summary": "One payout",
        "parameters": [ { "name": "id", "in": "path", "required": true, "description": "PO-...", "schema": { "type": "string" } } ],
        "responses": {
          "200": { "description": "Payout", "content": { "application/json": { "schema": { "type": "object", "properties": { "payout": { "$ref": "#/components/schemas/Payout" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/payouts/quote": {
      "post": {
        "tags": ["Payouts"], "operationId": "quotePayout", "x-section": "payout",
        "summary": "The exact fee and checks for one payout, moving nothing",
        "description": "Free to call: show the fee to your customer before they confirm.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PayoutQuoteRequest" } } } },
        "responses": {
          "200": { "description": "Quote", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PayoutQuote" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/payouts/limits": {
      "get": {
        "tags": ["Payouts"], "operationId": "getPayoutLimits", "x-section": "payout",
        "summary": "Your payout limits and today's use",
        "description": "Set per merchant by HotVoucher. maxPerRequestUsd - one payout at most (default 10 000 USD); dailyUsd - rolling 24 h cap (0 = none); autoApproveUsd - payouts up to it are approved at once, above it they wait for HotVoucher (status review).",
        "responses": {
          "200": { "description": "Limits", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PayoutLimits" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/payouts/lock": {
      "get": {
        "tags": ["Payouts"], "operationId": "getPayoutLock", "x-section": "payout",
        "summary": "State of the emergency stop",
        "responses": {
          "200": { "description": "Lock state", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LockState" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Payouts"], "operationId": "lockPayouts", "x-section": "payout",
        "summary": "EMERGENCY STOP for every payout of your account",
        "description": "Use it the moment you suspect a leaked token. From now on every payout of this account (and every withdrawal of its owner) waits for HotVoucher's approval and the open ones are pulled back; transfers and vouchers are refused (423 RISK_REVIEW). Only HotVoucher support releases it. The same stop is a button on the merchant panel dashboard.",
        "responses": {
          "200": { "description": "Locked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LockState" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/coins": {
      "get": {
        "tags": ["Coins and prices"], "operationId": "listCoins", "x-section": "inquiry",
        "summary": "Every coin and network, and whether each one is active right now",
        "description": "Hide or show a payment method on your site accordingly. Deposits that still arrive on a disabled network are credited as usual, but new addresses and invoices on it are refused (BAD_METHOD). Webhook event coin.status on every change; after any outage confirm the current state here.",
        "responses": {
          "200": { "description": "Coins", "content": { "application/json": { "schema": { "type": "object", "properties": { "coins": { "type": "array", "items": { "$ref": "#/components/schemas/Coin" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/coins/{coin}": {
      "get": {
        "tags": ["Coins and prices"], "operationId": "getCoin", "x-section": "inquiry",
        "summary": "One coin",
        "parameters": [ { "name": "coin", "in": "path", "required": true, "schema": { "type": "string", "example": "USDT" } } ],
        "responses": {
          "200": { "description": "Coin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Coin" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/prices": {
      "get": {
        "tags": ["Coins and prices"], "operationId": "listPrices", "x-section": "inquiry",
        "summary": "Live USD price of every coin, and the IRR rate",
        "description": "usd = HotVoucher's current price; null when no price is available right now (deposits are still credited, but payouts, priced invoices and swaps involving the coin are refused: NO_PRICE). toman / rial = usd converted with the current USDT to IRR rate; stale = that rate is older than usual.",
        "parameters": [ { "name": "coins", "in": "query", "description": "Comma-separated symbols; all coins when empty.", "schema": { "type": "string", "example": "BTC,USDT" } } ],
        "responses": {
          "200": { "description": "Prices", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Prices" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/prices/{coin}": {
      "get": {
        "tags": ["Coins and prices"], "operationId": "getPrice", "x-section": "inquiry",
        "summary": "Price of one coin",
        "parameters": [ { "name": "coin", "in": "path", "required": true, "schema": { "type": "string", "example": "BTC" } } ],
        "responses": {
          "200": { "description": "Price", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PriceSingle" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/swap/config": {
      "get": {
        "tags": ["Swap"], "operationId": "getSwapConfig", "x-section": "swap",
        "summary": "Coins, pairs, fees, limit and your balances",
        "responses": {
          "200": { "description": "Config", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SwapConfig" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/swap/quote": {
      "post": {
        "tags": ["Swap"], "operationId": "quoteSwap", "x-section": "swap",
        "summary": "Quote a conversion",
        "description": "mode \"from\" (default) = amount is what you give; mode \"to\" = amount is exactly what you want to receive. A quote is indicative: the swap uses the rate of the moment it arrives. Own coins: converting into HTV pays +4.5 %, into UUSD +4 % (feePct below 0 = that bonus, already inside rate and amountTo); HTV or UUSD into any other coin costs 6 %; UUSD to HTV is free (1:1); HTV to UUSD costs 0.6 %. HTV and UUSD are worth 1 USDT each.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SwapQuoteRequest" }, "example": { "from": "USDT", "to": "HTV", "amount": 100 } } } },
        "responses": {
          "200": { "description": "Quote", "content": { "application/json": { "schema": { "type": "object", "properties": { "quote": { "$ref": "#/components/schemas/SwapQuote" }, "maxUsd": { "type": "number" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/swap": {
      "post": {
        "tags": ["Swap"], "operationId": "createSwap", "x-section": "swap",
        "summary": "Convert between your own coins",
        "description": "idempotencyKey works exactly as for payouts: the same key returns the same swap again (replay: true) and never converts twice; the same key with a different body = 409 IDEMPOTENCY_MISMATCH.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SwapRequest" }, "example": { "idempotencyKey": "sw-5521", "from": "USDT", "to": "HTV", "amount": 100, "reference": "treasury" } } } },
        "responses": {
          "201": { "description": "Swap done", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SwapResult" } } } },
          "200": { "description": "Replay of an existing idempotencyKey", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SwapResult" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/Conflict" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/swaps": {
      "get": {
        "tags": ["Swap"], "operationId": "listSwaps", "x-section": "swap",
        "summary": "Your API swaps, newest first",
        "parameters": [ { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1 } } ],
        "responses": {
          "200": { "description": "Swaps", "content": { "application/json": { "schema": { "type": "object", "properties": { "swaps": { "type": "array", "items": { "$ref": "#/components/schemas/Swap" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/webhooks/test": {
      "post": {
        "tags": ["Webhooks"], "operationId": "testWebhook", "x-section": "inquiry",
        "summary": "Queue one \"test\" callback to the token's URL",
        "description": "Verify its signature exactly as for real events. 400 NO_CALLBACK = the token has no URL; CALLBACK_DISABLED = it was switched off after repeated failures (re-enable it in the app).",
        "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "note": { "type": "string", "description": "Any short text; it comes back in data.note." } } } } } },
        "responses": {
          "200": { "description": "Queued", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "queued": { "type": "boolean" }, "event": { "type": "string", "example": "test" }, "url": { "type": "string" }, "data": { "$ref": "#/components/schemas/TestEventData" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/webhooks": {
      "get": {
        "tags": ["Webhooks"], "operationId": "listWebhookDeliveries", "x-section": "inquiry",
        "summary": "The last deliveries of this token",
        "parameters": [
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["all", "pending", "done", "failed", "stopped"], "default": "all" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1 } }
        ],
        "responses": {
          "200": { "description": "Deliveries", "content": { "application/json": { "schema": { "type": "object", "properties": { "deliveries": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookDelivery" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/docs": {
      "get": {
        "tags": ["Guide"], "operationId": "getGuide", "security": [],
        "summary": "The plain-text guide",
        "description": "No token needed.",
        "responses": { "200": { "description": "The guide", "content": { "text/plain": { "schema": { "type": "string" } } } } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "hvk_...", "description": "Your API token (the secret key, starts with hvk_), in the header only: Authorization: Bearer hvk_YOUR_TOKEN. A token in the URL or in the body is refused. Requests are accepted only from the IP addresses whitelisted for the token. Keep the token on your server." }
    },
    "responses": {
      "BadRequest": { "description": "Bad request - the message says why (codes such as BAD_INPUT, BAD_CODE, BAD_HASH, HASH_REQUIRED, BAD_AMOUNT, BAD_METHOD, BAD_COIN, SAME_COIN, PAIR_OFF, INSUFFICIENT, NO_PRICE, REJECTED, PAYOUT_CAP, PAYOUT_LIMIT, SWAP_LIMIT, NO_CALLBACK, CALLBACK_DISABLED, LOCK_NOT_FOUND, VOUCHER_USED, HOTVOUCHER_VOUCHER)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unauthorized": { "description": "NO_TOKEN (no hvk_ token in the Authorization header) or BAD_TOKEN (invalid or revoked)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Invalid or revoked token", "code": "BAD_TOKEN" } } } },
      "Forbidden": { "description": "IP_NOT_ALLOWED (the caller's IP is not whitelisted for the token), SECTION_NOT_ALLOWED (the token lacks the section; the answer names it), ACCOUNT_BLOCKED, SECURITY_LOCK (write calls pause only when the API settings were changed during the 24-hour security lock that follows an account security change - until that lock ends, see until; also while an admin reviews the account), LOCKED (a voucher locked to another account)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "This token has no access to this API section", "code": "SECTION_NOT_ALLOWED", "section": "payout" } } } },
      "NotFound": { "description": "NOT_FOUND", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Conflict": { "description": "ALREADY_USED, CONFLICT, IDEMPOTENCY_MISMATCH (the same idempotencyKey with a different body), VOUCHER_BUSY", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Gone": { "description": "VOID or EXPIRED voucher", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Locked": { "description": "RISK_REVIEW - the account is under review (for example after the emergency stop); transfers and vouchers are refused until HotVoucher releases it", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": { "description": "RATE_LIMIT - over the token's limit; slow down and retry later", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unavailable": { "description": "FROZEN (maintenance) or the payment gateway is disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    },
    "schemas": {
      "Error": {
        "type": "object", "required": ["error"],
        "properties": {
          "error": { "type": "string", "description": "Human-readable message." },
          "code": { "type": "string", "description": "Machine-readable code.", "example": "BAD_INPUT" },
          "section": { "type": "string", "description": "With SECTION_NOT_ALLOWED: the section the call needs." }
        }
      },
      "Me": {
        "type": "object",
        "properties": {
          "merchant": { "type": "object", "properties": { "id": { "type": "string", "example": "HV-ABC123" }, "name": { "type": "string" } } },
          "token": { "$ref": "#/components/schemas/Token" }
        }
      },
      "Token": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" }, "name": { "type": "string" }, "publicName": { "type": "boolean", "description": "The name may be shown on voucher lookups." },
          "prefix": { "type": "string", "description": "The first characters of the token, for recognition." },
          "sections": { "type": "array", "items": { "type": "string", "enum": ["inquiry", "code_inquiry", "code_redeem", "code_issue", "merchant", "address", "payout", "swap"] } },
          "siteUrl": { "type": "string" }, "ips": { "type": "array", "items": { "type": "string" }, "description": "Whitelisted IP addresses." },
          "cashback": { "type": "number", "description": "Cashback percent on redeemed vouchers." },
          "status": { "type": "string", "example": "active" }, "disabledAt": { "type": "integer", "format": "int64", "nullable": true },
          "createdAt": { "type": "integer", "format": "int64" }, "lastUsed": { "type": "integer", "format": "int64", "nullable": true }, "uses": { "type": "integer" },
          "callbackUrl": { "type": "string" }, "callbackEnabled": { "type": "boolean" }, "callbackFailStreak": { "type": "integer" }, "hasCallbackSecret": { "type": "boolean" },
          "lastIp": { "type": "string" }
        }
      },
      "Balances": {
        "type": "object",
        "properties": { "balances": { "type": "object", "description": "Coin symbol -> amount.", "additionalProperties": { "type": "number" } } }
      },
      "Transaction": {
        "type": "object",
        "properties": {
          "id": { "type": "string" }, "hash": { "type": "string" }, "ts": { "type": "integer", "format": "int64", "description": "Unix time in milliseconds." },
          "type": { "type": "string", "description": "pay_in, pay_fee, withdraw, swap, voucher, api_redeem, internal_in, internal_out, deposit, addr_fee, ..." },
          "coin": { "type": "string" }, "amount": { "type": "number", "description": "Signed: negative = out." }, "note": { "type": "string" }, "ref": { "type": "string" },
          "network": { "type": "string", "nullable": true }
        }
      },
      "Statement": {
        "type": "object",
        "properties": {
          "from": { "type": "integer", "format": "int64" }, "to": { "type": "integer", "format": "int64" },
          "coins": { "type": "array", "items": { "type": "object", "properties": {
            "coin": { "type": "string" }, "opening": { "type": "number" }, "closing": { "type": "number" }, "in": { "type": "number" }, "out": { "type": "number" }, "net": { "type": "number" }, "count": { "type": "integer" },
            "byType": { "type": "object", "description": "Transaction type -> {n, amount}.", "additionalProperties": { "type": "object", "properties": { "n": { "type": "integer" }, "amount": { "type": "number" } } } }
          } } }
        }
      },
      "CheckRequest": {
        "type": "object", "description": "Send code or ref.",
        "properties": { "code": { "type": "string", "example": "USD-XXXX-XXXX-XXXX-XXXX-XXXX" }, "ref": { "type": "string", "example": "VC-8F3K2M9Q" } }
      },
      "VoucherCheck": {
        "type": "object",
        "properties": {
          "found": { "type": "boolean" }, "status": { "type": "string", "enum": ["active", "redeemed", "void", "expired"] }, "amount": { "type": "number" }, "coin": { "type": "string" }, "ref": { "type": "string" }, "locked": { "type": "boolean" },
          "createdAt": { "type": "integer", "format": "int64", "description": "Only for your own vouchers." },
          "redeemedAt": { "type": "integer", "format": "int64", "nullable": true, "description": "Only for your own vouchers." },
          "lockedTo": { "type": "string", "nullable": true, "description": "Only for your own vouchers." },
          "redeemedBy": { "type": "object", "nullable": true, "description": "Only for your own vouchers.", "properties": { "id": { "type": "string" }, "site": { "type": "string", "nullable": true } } },
          "redeemedVia": { "type": "string", "nullable": true }
        }
      },
      "VoucherInquiryRequest": {
        "type": "object", "required": ["code"],
        "properties": {
          "code": { "type": "string", "description": "A HotVoucher voucher or a Utopia uVoucher code.", "example": "USD-XXXX-XXXX-XXXX-XXXX-XXXX" },
          "kind": { "type": "string", "enum": ["hotvoucher", "uvoucher"], "description": "Optional: which kind the code is. Without it HotVoucher vouchers are looked up first." },
          "hash": { "type": "string", "description": "Required for a uVoucher: the hash of the Utopia transaction that created it (16-160 letters and digits); the Utopia network checks the code against it." }
        }
      },
      "VoucherInquiry": {
        "type": "object", "description": "kind hotvoucher carries the fields of VoucherCheck (ref, locked and, for your own vouchers, the parties); kind uvoucher the fields below.",
        "properties": {
          "kind": { "type": "string", "enum": ["hotvoucher", "uvoucher"] },
          "checkedBy": { "type": "string", "enum": ["hotvoucher", "utopia-network"], "description": "hotvoucher = a HotVoucher voucher, from HotVoucher's own records; utopia-network = a uVoucher, the network's own record." },
          "found": { "type": "boolean" },
          "status": { "type": "string", "enum": ["active", "redeemed", "void", "expired", "not_found", "unknown"], "description": "active = can still be redeemed; redeemed = already used; void / expired = HotVoucher vouchers only; not_found = the network has no voucher with this code in that transaction; unknown = not a HotVoucher voucher and sent without hash (a uVoucher always needs it)." },
          "amount": { "type": "number" }, "coin": { "type": "string", "example": "UUSD" },
          "createdAt": { "type": "integer", "format": "int64", "description": "Milliseconds; uVouchers when known, HotVoucher vouchers only for your own." },
          "redeemedAt": { "type": "integer", "format": "int64", "nullable": true },
          "ref": { "type": "string", "description": "HotVoucher vouchers." }, "locked": { "type": "boolean", "description": "HotVoucher vouchers." },
          "needHash": { "type": "boolean", "description": "true = not a HotVoucher voucher (sent without kind); a uVoucher needs hash." },
          "message": { "type": "string" },
          "inquiryId": { "type": "string", "description": "Network inquiries: the id of this inquiry." },
          "cached": { "type": "boolean", "description": "true = the same inquiry was answered a moment ago and kept in memory (the network was not asked again)." }
        }
      },
      "VoucherInquiryQueued": {
        "type": "object",
        "properties": {
          "kind": { "type": "string", "enum": ["uvoucher"] },
          "status": { "type": "string", "enum": ["queued", "checking"] },
          "inquiryId": { "type": "string", "example": "UVQ-3F9A1C7B2D4E" },
          "position": { "type": "integer", "description": "Place in the line; 0 = being checked now." },
          "retryAfter": { "type": "integer", "description": "Seconds to wait before GET /vouchers/inquiry/{inquiryId} (also the Retry-After header)." }
        }
      },
      "RedeemRequest": {
        "type": "object", "required": ["code"],
        "properties": { "code": { "type": "string", "description": "20 letters / digits; the USD- (or old HTV-) prefix and the dashes are optional.", "example": "USD-XXXX-XXXX-XXXX-XXXX-XXXX" }, "reference": { "type": "string", "maxLength": 80, "description": "Your own reference (order id)." } }
      },
      "RedeemResult": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" }, "amount": { "type": "number" }, "ref": { "type": "string" }, "txId": { "type": "string" }, "txHash": { "type": "string" },
          "balance": { "type": "number", "description": "Your balance in the voucher's coin after the credit." },
          "cashback": { "type": "object", "nullable": true, "properties": { "percent": { "type": "number" }, "amount": { "type": "number" }, "to": { "type": "string" }, "txId": { "type": "string" } } }
        }
      },
      "VoucherCreateRequest": {
        "type": "object", "required": ["amount"],
        "properties": {
          "amount": { "type": "number", "minimum": 0.1, "description": "HTV, at least 0.1." },
          "lockTo": { "type": "string", "description": "The customer's HV-... id; only that account can redeem it." },
          "reference": { "type": "string", "maxLength": 80 }
        }
      },
      "VoucherCreated": {
        "type": "object",
        "properties": { "ok": { "type": "boolean" }, "code": { "type": "string" }, "ref": { "type": "string" }, "amount": { "type": "number" }, "lockedTo": { "type": "string", "nullable": true }, "txId": { "type": "string" }, "txHash": { "type": "string" }, "balance": { "type": "number" } }
      },
      "VoucherCancelRequest": { "type": "object", "required": ["ref"], "properties": { "ref": { "type": "string", "example": "VC-8F3K2M9Q" } } },
      "Voucher": {
        "type": "object",
        "properties": {
          "ref": { "type": "string" }, "code": { "type": "string" }, "reference": { "type": "string" }, "lockedTo": { "type": "string", "nullable": true },
          "amount": { "type": "number" }, "coin": { "type": "string" }, "status": { "type": "string", "enum": ["active", "redeemed", "void", "expired"] },
          "createdAt": { "type": "integer", "format": "int64" }, "expiresAt": { "type": "integer", "format": "int64", "nullable": true }, "redeemedAt": { "type": "integer", "format": "int64", "nullable": true },
          "createTx": { "type": "string" }, "redeemTx": { "type": "string" }
        }
      },
      "InvoiceCreateRequest": {
        "type": "object", "required": ["customer"],
        "properties": {
          "customer": { "type": "string", "maxLength": 64, "description": "Your user id / username; decides the customer's permanent deposit address." },
          "amount": { "type": "number", "description": "Optional; with an amount, currency is required. Without = open top-up invoice." },
          "currency": { "type": "string", "enum": ["USD", "IRR"] },
          "orderId": { "type": "string" }, "description": { "type": "string" },
          "returnUrl": { "type": "string", "format": "uri", "description": "https URL for the Back to shop button." },
          "expiresInMin": { "type": "integer" }, "lang": { "type": "string", "enum": ["en", "fa"] }
        }
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "id": { "type": "string" }, "status": { "type": "string", "enum": ["pending", "paid", "expired"] }, "customer": { "type": "string" }, "orderId": { "type": "string", "nullable": true }, "description": { "type": "string", "nullable": true },
          "amount": { "type": "number", "nullable": true }, "currency": { "type": "string", "nullable": true }, "amountUsd": { "type": "number", "nullable": true },
          "paidUsd": { "type": "number" }, "remainingUsd": { "type": "number", "nullable": true }, "feeUsd": { "type": "number" },
          "credited": { "type": "array", "description": "What landed in your account, per coin.", "items": { "$ref": "#/components/schemas/CoinAmount" } },
          "settledHtv": { "type": "number", "description": "Kept for old integrations." },
          "createdAt": { "type": "integer", "format": "int64" }, "expiresAt": { "type": "integer", "format": "int64" }, "paidAt": { "type": "integer", "format": "int64", "nullable": true },
          "lateUntil": { "type": "integer", "format": "int64", "nullable": true, "description": "Set while the invoice is past expiresAt but kept open (status stays pending) because a payment seen before its end is still confirming on the network; re-checked every 5 minutes, at most 24 hours past the end. The payment then completes this invoice; invoice.expired is sent only if it never arrives. null otherwise." },
          "returnUrl": { "type": "string", "nullable": true }, "linkId": { "type": "string", "nullable": true }, "note": { "type": "string", "nullable": true },
          "payUrl": { "type": "string", "description": "In the create answer: send the buyer here (full-page redirect)." }
        }
      },
      "InvoiceDetail": {
        "allOf": [ { "$ref": "#/components/schemas/Invoice" }, { "type": "object", "properties": {
          "payments": { "type": "array", "items": { "type": "object", "properties": { "ref": { "type": "string" }, "method": { "type": "string" }, "coin": { "type": "string" }, "network": { "type": "string" }, "amount": { "type": "number" }, "usd": { "type": "number" }, "feeUsd": { "type": "number" }, "settledHtv": { "type": "number" }, "createdAt": { "type": "integer", "format": "int64" } } } },
          "detected": { "type": "array", "items": { "$ref": "#/components/schemas/PendingPayment" } }
        } } ]
      },
      "CoinAmount": { "type": "object", "properties": { "coin": { "type": "string" }, "amount": { "type": "number" } } },
      "Payment": {
        "type": "object",
        "properties": {
          "ref": { "type": "string" }, "invoice": { "type": "string", "nullable": true }, "customer": { "type": "string" }, "method": { "type": "string", "description": "crypto, wallet or voucher" },
          "coin": { "type": "string" }, "network": { "type": "string" }, "amount": { "type": "number" }, "amountUsd": { "type": "number" }, "feeUsd": { "type": "number" }, "networkFeeUsd": { "type": "number" }, "addressFeeUsd": { "type": "number" },
          "credited": { "$ref": "#/components/schemas/CoinAmount" }, "creditCoin": { "type": "string" }, "creditAmount": { "type": "number" }, "convertRef": { "type": "string", "nullable": true },
          "settledHtv": { "type": "number" }, "status": { "type": "string" }, "createdAt": { "type": "integer", "format": "int64" }
        }
      },
      "PendingPayment": {
        "type": "object",
        "properties": {
          "customer": { "type": "string" }, "invoice": { "type": "string", "nullable": true }, "coin": { "type": "string" }, "network": { "type": "string" }, "amount": { "type": "number" },
          "confirmations": { "type": "integer" }, "required": { "type": "integer" }, "txId": { "type": "string" }, "explorer": { "type": "string" }, "seenAt": { "type": "integer", "format": "int64" },
          "verifying": { "type": "boolean", "description": "true while the explorer check runs." }
        }
      },
      "Fees": {
        "type": "object",
        "properties": {
          "gatewayFeePct": { "type": "number" }, "addressFeeUsd": { "type": "number", "example": 0 },
          "networkFees": { "type": "array", "items": { "type": "object", "properties": { "coin": { "type": "string" }, "network": { "type": "string" }, "networkFeeUsd": { "type": "number" } } } }
        }
      },
      "Address": {
        "type": "object",
        "properties": { "customer": { "type": "string" }, "chain": { "type": "string" }, "networks": { "type": "array", "items": { "type": "string" } }, "address": { "type": "string" }, "memo": { "type": "string", "nullable": true }, "createdAt": { "type": "integer", "format": "int64" } }
      },
      "AddressValidation": {
        "type": "object",
        "properties": { "valid": { "type": "boolean" }, "validOn": { "type": "array", "items": { "type": "string" } }, "mine": { "type": "boolean" }, "customer": { "type": "string", "nullable": true }, "chain": { "type": "string", "nullable": true }, "networks": { "type": "array", "items": { "type": "string" } } }
      },
      "CustomerAddressRequest": {
        "type": "object", "required": ["customer", "coin", "network"],
        "properties": { "customer": { "type": "string", "maxLength": 64 }, "coin": { "type": "string", "example": "USDT" }, "network": { "type": "string", "example": "TRC20" } }
      },
      "CustomerAddress": {
        "type": "object",
        "properties": {
          "customer": { "type": "string" }, "coin": { "type": "string" }, "network": { "type": "string" }, "address": { "type": "string" }, "memo": { "type": "string", "nullable": true },
          "minDeposit": { "type": "number" }, "networkFeeUsd": { "type": "number" },
          "others": { "type": "array", "description": "Other coins / networks that share this address.", "items": { "type": "object", "additionalProperties": true } }
        }
      },
      "UusdVoucherRequest": { "type": "object", "required": ["customer", "code"], "properties": { "customer": { "type": "string", "maxLength": 64 }, "code": { "type": "string", "example": "USD-XXXX-XXXX-XXXX-XXXX-XXXX" } } },
      "UusdVoucherResult": {
        "type": "object",
        "properties": { "ok": { "type": "boolean" }, "pending": { "type": "boolean" }, "customer": { "type": "string" }, "coin": { "type": "string", "example": "UUSD" }, "network": { "type": "string", "example": "Utopia" }, "amount": { "type": "number" }, "reference": { "type": "string" } }
      },
      "UusdVerifyRequest": { "type": "object", "required": ["customer", "hash"], "properties": { "customer": { "type": "string", "maxLength": 64 }, "hash": { "type": "string" }, "code": { "type": "string" } } },
      "PayoutRequest": {
        "type": "object", "required": ["coin", "network", "address", "amount"],
        "properties": {
          "idempotencyKey": { "type": "string", "minLength": 1, "maxLength": 80, "description": "Required except in a dry run: unique per payout on your side (your withdrawal id)." },
          "coin": { "type": "string" }, "network": { "type": "string" }, "address": { "type": "string" },
          "amount": { "type": "number", "description": "In coin units, what your customer asked for." },
          "feePayer": { "type": "string", "enum": ["customer", "merchant"], "default": "customer" },
          "memo": { "type": "string" }, "customer": { "type": "string" }, "reference": { "type": "string" },
          "dryRun": { "type": "boolean", "description": "Run every check, move nothing." }
        }
      },
      "Payout": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "PO-..." }, "status": { "type": "string", "enum": ["approved", "review", "sending", "sent", "failed"] },
          "coin": { "type": "string" }, "network": { "type": "string" }, "address": { "type": "string" }, "memo": { "type": "string", "nullable": true },
          "amount": { "type": "number", "description": "Requested." }, "fee": { "type": "number", "description": "Network fee actually charged." }, "feeUsd": { "type": "number", "nullable": true },
          "feePayer": { "type": "string", "enum": ["customer", "merchant"] }, "receives": { "type": "number", "description": "What the address gets." }, "debited": { "type": "number", "description": "What left your balance." },
          "total": { "type": "number", "description": "Kept for old integrations = debited." }, "amountUsd": { "type": "number", "nullable": true },
          "kind": { "type": "string", "enum": ["chain", "internal"] }, "txId": { "type": "string", "nullable": true }, "failReason": { "type": "string", "nullable": true }, "customer": { "type": "string", "nullable": true },
          "voucher": { "type": "string", "description": "Payouts to UVOUCHER (Utopia network): the code, once made." }, "voucherState": { "type": "string", "enum": ["unused", "used", "unknown"] },
          "replay": { "type": "boolean", "description": "true when an existing idempotencyKey returned this payout." }
        }
      },
      "PayoutDryRun": {
        "type": "object",
        "properties": { "dryRun": { "type": "boolean", "example": true }, "ok": { "type": "boolean" }, "expectedStatus": { "type": "string", "enum": ["approved", "review", "sent"] }, "quote": { "$ref": "#/components/schemas/PayoutQuote" }, "amountUsd": { "type": "number" }, "limits": { "$ref": "#/components/schemas/PayoutLimits" } }
      },
      "PayoutQuoteRequest": {
        "type": "object", "required": ["coin", "network", "address", "amount"],
        "properties": { "coin": { "type": "string" }, "network": { "type": "string" }, "address": { "type": "string" }, "amount": { "type": "number" }, "feePayer": { "type": "string", "enum": ["customer", "merchant"], "default": "customer" } }
      },
      "PayoutQuote": {
        "type": "object",
        "properties": {
          "validAddress": { "type": "boolean" }, "available": { "type": "boolean" }, "reason": { "type": "string", "nullable": true }, "fee": { "type": "number" }, "receives": { "type": "number" }, "total": { "type": "number" },
          "min": { "type": "number", "description": "Network minimum." }, "internal": { "type": "boolean", "description": "The address belongs to a HotVoucher account (fee 0)." }, "maxUsd": { "type": "number" },
          "feePayer": { "type": "string", "enum": ["customer", "merchant"] }, "limits": { "$ref": "#/components/schemas/PayoutLimits" }
        }
      },
      "PayoutLimits": {
        "type": "object",
        "properties": { "maxPerRequestUsd": { "type": "number" }, "dailyUsd": { "type": "number", "description": "0 = no daily cap." }, "autoApproveUsd": { "type": "number" }, "usedTodayUsd": { "type": "number" }, "remainingTodayUsd": { "type": "number", "nullable": true } }
      },
      "LockState": { "type": "object", "properties": { "locked": { "type": "boolean" }, "since": { "type": "integer", "format": "int64", "nullable": true }, "reason": { "type": "string", "nullable": true } } },
      "Coin": {
        "type": "object",
        "properties": {
          "coin": { "type": "string" }, "status": { "type": "string", "enum": ["active", "disabled"] }, "reason": { "type": "string", "nullable": true }, "since": { "type": "integer", "format": "int64", "nullable": true }, "price": { "type": "number", "nullable": true },
          "networks": { "type": "array", "items": { "$ref": "#/components/schemas/CoinNetwork" } }
        }
      },
      "CoinNetwork": {
        "type": "object",
        "properties": { "network": { "type": "string" }, "status": { "type": "string", "enum": ["active", "disabled"] }, "reason": { "type": "string", "nullable": true }, "since": { "type": "integer", "format": "int64", "nullable": true }, "decimals": { "type": "integer" }, "min": { "type": "number" }, "networkFeeUsd": { "type": "number" } }
      },
      "Price": {
        "type": "object",
        "properties": { "coin": { "type": "string" }, "usd": { "type": "number", "nullable": true }, "toman": { "type": "number", "nullable": true }, "rial": { "type": "number", "nullable": true }, "source": { "type": "string" }, "halted": { "type": "boolean" } }
      },
      "IrrRate": {
        "type": "object",
        "properties": { "usdtToman": { "type": "number" }, "usdtRial": { "type": "number" }, "updatedAt": { "type": "integer", "format": "int64" }, "stale": { "type": "boolean" }, "source": { "type": "string" } }
      },
      "Prices": {
        "type": "object",
        "properties": { "at": { "type": "integer", "format": "int64" }, "prices": { "type": "array", "items": { "$ref": "#/components/schemas/Price" } }, "irr": { "$ref": "#/components/schemas/IrrRate" } }
      },
      "PriceSingle": {
        "allOf": [ { "$ref": "#/components/schemas/Price" }, { "type": "object", "properties": { "at": { "type": "integer", "format": "int64" }, "irr": { "$ref": "#/components/schemas/IrrRate" } } } ]
      },
      "SwapConfig": {
        "type": "object",
        "properties": {
          "coins": { "type": "array", "items": { "type": "string" } },
          "pairs": { "type": "object", "description": "FROM -> [TO, ...]", "additionalProperties": { "type": "array", "items": { "type": "string" } } },
          "fees": { "type": "object", "description": "COIN -> {pct, fixed}", "additionalProperties": { "type": "object", "properties": { "pct": { "type": "number" }, "fixed": { "type": "number" } } } },
          "maxUsd": { "type": "number" }, "balances": { "type": "object", "additionalProperties": { "type": "number" } }
        }
      },
      "SwapQuoteRequest": {
        "type": "object", "required": ["from", "to", "amount"],
        "properties": { "from": { "type": "string" }, "to": { "type": "string" }, "amount": { "type": "number" }, "mode": { "type": "string", "enum": ["from", "to"], "default": "from" } }
      },
      "SwapQuote": {
        "type": "object",
        "properties": { "from": { "type": "string" }, "to": { "type": "string" }, "amountFrom": { "type": "number" }, "amountTo": { "type": "number" }, "rate": { "type": "number" }, "feePct": { "type": "number" }, "feeFixed": { "type": "number" }, "feeAmount": { "type": "number" }, "priceFrom": { "type": "number" }, "priceTo": { "type": "number" }, "amountUsd": { "type": "number" } }
      },
      "SwapRequest": {
        "type": "object", "required": ["idempotencyKey", "from", "to", "amount"],
        "properties": { "idempotencyKey": { "type": "string", "minLength": 1, "maxLength": 80 }, "from": { "type": "string" }, "to": { "type": "string" }, "amount": { "type": "number" }, "mode": { "type": "string", "enum": ["from", "to"], "default": "from" }, "reference": { "type": "string" } }
      },
      "Swap": {
        "type": "object",
        "properties": { "ref": { "type": "string", "example": "SW-..." }, "from": { "type": "string" }, "to": { "type": "string" }, "amountFrom": { "type": "number" }, "amountTo": { "type": "number" }, "rate": { "type": "number" }, "feeAmount": { "type": "number" }, "amountUsd": { "type": "number" }, "mode": { "type": "string" }, "reference": { "type": "string", "nullable": true }, "createdAt": { "type": "integer", "format": "int64" } }
      },
      "SwapResult": {
        "type": "object",
        "properties": { "swap": { "$ref": "#/components/schemas/Swap" }, "balances": { "type": "object", "additionalProperties": { "type": "number" } }, "replay": { "type": "boolean" } }
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": { "id": { "type": "integer" }, "event": { "type": "string" }, "status": { "type": "string", "enum": ["pending", "done", "failed", "stopped"] }, "attempts": { "type": "integer" }, "lastCode": { "type": "integer", "nullable": true }, "lastError": { "type": "string", "nullable": true }, "createdAt": { "type": "integer", "format": "int64" }, "lastAt": { "type": "integer", "format": "int64", "nullable": true }, "nextAt": { "type": "integer", "format": "int64", "nullable": true }, "doneAt": { "type": "integer", "format": "int64", "nullable": true }, "ref": { "type": "string", "nullable": true }, "txId": { "type": "string", "nullable": true } }
      },
      "WebhookEnvelope": {
        "type": "object",
        "properties": {
          "event": { "type": "string", "enum": ["redeem", "voucher_created", "voucher_cancelled", "payment.detected", "payment.received", "invoice.paid", "invoice.expired", "coin.status", "payout.status", "uvoucher.failed", "test"] },
          "at": { "type": "integer", "format": "int64" },
          "token": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" } } },
          "merchant": { "type": "object", "properties": { "id": { "type": "string" } } },
          "data": { "type": "object", "description": "Per event - see the events list.", "additionalProperties": true }
        }
      },
      "PaymentDetectedData": {
        "type": "object",
        "properties": { "customer": { "type": "string" }, "invoice": { "type": "string", "nullable": true }, "orderId": { "type": "string", "nullable": true }, "coin": { "type": "string" }, "network": { "type": "string" }, "amount": { "type": "number" }, "confirmations": { "type": "integer" }, "required": { "type": "integer" }, "txId": { "type": "string" }, "explorer": { "type": "string" }, "seenAt": { "type": "integer", "format": "int64" } }
      },
      "PaymentReceivedData": {
        "type": "object",
        "properties": {
          "ref": { "type": "string" }, "invoice": { "type": "string", "nullable": true }, "orderId": { "type": "string", "nullable": true }, "customer": { "type": "string" }, "method": { "type": "string" },
          "coin": { "type": "string" }, "network": { "type": "string" }, "amount": { "type": "number" }, "amountUsd": { "type": "number" }, "feeUsd": { "type": "number" }, "networkFeeUsd": { "type": "number" }, "addressFeeUsd": { "type": "number" },
          "credited": { "$ref": "#/components/schemas/CoinAmount" },
          "converted": { "type": "object", "nullable": true, "description": "When automatic conversion is on.", "properties": { "from": { "type": "string" }, "to": { "type": "string" }, "amountFrom": { "type": "number" }, "amountTo": { "type": "number" }, "rate": { "type": "number" }, "feeAmount": { "type": "number" }, "ref": { "type": "string" } } },
          "settledHtv": { "type": "number", "description": "Kept for old integrations." },
          "txId": { "type": "string", "nullable": true }, "txHash": { "type": "string", "nullable": true, "description": "The chain transaction hash of a crypto payment; null for wallet / voucher." }
        }
      },
      "UvoucherFailedData": { "type": "object", "properties": { "customer": { "type": "string" }, "reference": { "type": "string" }, "voucher": { "type": "string", "description": "Masked." }, "at": { "type": "integer", "format": "int64" } } },
      "TestEventData": { "type": "object", "properties": { "message": { "type": "string" }, "note": { "type": "string", "nullable": true }, "at": { "type": "integer", "format": "int64" } } }
    }
  },
  "x-webhooks": {
    "description": "If a callback URL is set on the token, every API transaction triggers one HTTPS POST with a JSON body (the envelope below) to that URL. Only HTTPS URLs on public hosts are accepted; redirects are not followed; timeout 8 seconds.",
    "envelope": { "$ref": "#/components/schemas/WebhookEnvelope" },
    "headers": [
      { "name": "X-HotVoucher-Event", "description": "The event name." },
      { "name": "X-HotVoucher-Delivery", "description": "Delivery id - unique per message, repeated on retries. Use it (or data.txId) to de-duplicate: a message may arrive more than once." },
      { "name": "X-HotVoucher-Attempt", "description": "Attempt number (1..13)." },
      { "name": "X-HotVoucher-Signature", "description": "Hex HMAC-SHA256 of the raw request body, key = your callback secret (hvcs_...)." }
    ],
    "signature": "Verify the signature on every callback: compute HMAC-SHA256(rawBody, callbackSecret) and compare it in constant time with X-HotVoucher-Signature. Reject the message if it does not match. The callback secret (hvcs_...) is used ONLY for this check and is never sent in requests.",
    "example": "const ok = crypto.timingSafeEqual(\n  Buffer.from(req.get('X-HotVoucher-Signature'), 'hex'),\n  crypto.createHmac('sha256', CALLBACK_SECRET).update(rawBody).digest()\n);",
    "retries": "Answer with any 2xx status = received. Otherwise we retry up to 13 times within 24 hours (immediately, then 1, 5, 10, 20, 30 minutes, 1, 2, 4, 6, 9, 14, 23 hours). If 3 consecutive transactions exhaust all attempts, the token's callback is disabled automatically; re-enable it in the app (edit the token).",
    "events": [
      { "name": "payment.detected", "description": "A crypto payment seen on the network, before confirmation - once per transaction.", "data": { "$ref": "#/components/schemas/PaymentDetectedData" } },
      { "name": "payment.received", "description": "Every payment, credited (for crypto: only after the coin's own explorer confirmed it).", "data": { "$ref": "#/components/schemas/PaymentReceivedData" } },
      { "name": "invoice.paid", "description": "data = the invoice, with credited per coin.", "data": { "$ref": "#/components/schemas/Invoice" } },
      { "name": "invoice.expired", "description": "data = the invoice, with credited per coin.", "data": { "$ref": "#/components/schemas/Invoice" } },
      { "name": "payout.status", "description": "Every status change of a payout. Treat sent and failed as final.", "data": { "$ref": "#/components/schemas/Payout" } },
      { "name": "coin.status", "description": "A coin or one of its networks switched between active and disabled.", "data": { "$ref": "#/components/schemas/Coin" } },
      { "name": "redeem", "description": "A voucher was redeemed through the API (the same object as the API answer).", "data": { "$ref": "#/components/schemas/RedeemResult" } },
      { "name": "voucher_created", "description": "A voucher was created through the API.", "data": { "$ref": "#/components/schemas/VoucherCreated" } },
      { "name": "voucher_cancelled", "description": "A voucher was cancelled through the API.", "data": { "type": "object", "additionalProperties": true } },
      { "name": "uvoucher.failed", "description": "A UUSD uVoucher sent with POST /customers/uusd-voucher did not activate (about 10 minutes later).", "data": { "$ref": "#/components/schemas/UvoucherFailedData" } },
      { "name": "test", "description": "Sent by POST /webhooks/test.", "data": { "$ref": "#/components/schemas/TestEventData" } }
    ]
  }
}
