{
  "openapi": "3.1.0",
  "info": {
    "title": "ZettaPay listener API",
    "version": "0.6.0",
    "summary": "Self-hosted, non-custodial crypto payment listener. Accept Bitcoin and USDC/USDT on Base. Funds settle straight to the merchant's wallet — the listener never holds keys or money.",
    "description": "The ZettaPay listener is an open-source HTTP service the merchant runs on their own box (`npm i -g @zettapay/listener`). It derives or reuses on-chain receive addresses from a public key the merchant supplies (a BIP-84 xpub for Bitcoin, an account xpub or a single receive address for USDC/USDT on Base), watches the chain, and fires an HMAC-signed webhook the moment a payment confirms.\n\n**Non-custodial.** The merchant only ever provides a public key. The listener never holds a signing key and never custodies funds. Payments settle directly to the merchant's wallet on-chain.\n\n**No identity checks, no telemetry, no third party.** Nothing phones home.\n\n**Auth.** When `ZETTAPAY_API_KEY` is configured, `POST /invoice` requires the `X-ZettaPay-Api-Key` header (compared in constant time). `GET /invoice/:id` and `GET /health` are unauthenticated.\n\n**Webhooks.** Each delivery to the merchant's URL is signed: `X-ZettaPay-Signature` is the hex HMAC-SHA256 of the raw request body keyed by `ZETTAPAY_WEBHOOK_SECRET`, `X-ZettaPay-Timestamp` is the delivery time in unix milliseconds, `X-ZettaPay-Event-Id` is a stable id for idempotency, and `X-ZettaPay-Attempt` is the 1-indexed attempt number.",
    "contact": {
      "name": "ZettaPay",
      "url": "https://github.com/leandromaiam-code/zettapay"
    },
    "license": {
      "name": "MIT",
      "url": "https://opensource.org/licenses/MIT"
    }
  },
  "servers": [
    {
      "url": "http://localhost:8787",
      "description": "Self-hosted listener (default port)"
    }
  ],
  "tags": [
    {
      "name": "Invoices",
      "description": "Create and poll on-chain invoices for Bitcoin and USDC/USDT on Base."
    },
    {
      "name": "Health",
      "description": "Liveness and watcher state."
    }
  ],
  "paths": {
    "/invoice": {
      "post": {
        "operationId": "createInvoice",
        "tags": ["Invoices"],
        "summary": "Create an invoice",
        "description": "Creates a pending invoice. `chain` is optional and defaults to `btc`. For Bitcoin pass `amount_sats`; for Base pass `amount_usd` (and optionally `asset` for USDT). The response carries the receive address and a payment/QR URI. Requires the `X-ZettaPay-Api-Key` header when `ZETTAPAY_API_KEY` is set.",
        "security": [{ "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  { "$ref": "#/components/schemas/CreateBtcInvoiceRequest" },
                  { "$ref": "#/components/schemas/CreateBaseInvoiceRequest" }
                ]
              },
              "examples": {
                "bitcoin": {
                  "summary": "Bitcoin",
                  "value": { "amount_sats": 50000, "memo": "order 4711", "expires_in": 3600 }
                },
                "usdc-base": {
                  "summary": "USDC on Base",
                  "value": { "chain": "base", "amount_usd": 42.0 }
                },
                "usdt-base": {
                  "summary": "USDT on Base (fixed-address mode)",
                  "value": { "chain": "base", "amount_usd": 42.0, "asset": "usdt" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Invoice created.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Invoice" }
              }
            }
          },
          "400": {
            "description": "Invalid amount, unsupported or disabled chain/asset, or malformed body.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": {
            "description": "Rate limited (sliding window per client IP). Honors the Retry-After header.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "503": {
            "description": "Fixed-address nonce pool temporarily exhausted — retry shortly.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "500": {
            "description": "Invoice could not be created.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/invoice/{id}": {
      "get": {
        "operationId": "getInvoice",
        "tags": ["Invoices"],
        "summary": "Get invoice status",
        "description": "Returns the current invoice. A pending invoice past its `expires_at` is lazily flipped to `expired` on read.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "The invoice id returned at creation."
          }
        ],
        "responses": {
          "200": {
            "description": "The invoice.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Invoice" } } }
          },
          "404": {
            "description": "No invoice with that id.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "health",
        "tags": ["Health"],
        "summary": "Liveness and watcher state",
        "description": "Unauthenticated liveness probe. Reports websocket connectivity and how many addresses are being watched.",
        "responses": {
          "200": {
            "description": "Listener is up.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Health" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-ZettaPay-Api-Key",
        "description": "Required for POST /invoice when ZETTAPAY_API_KEY is configured on the listener."
      }
    },
    "schemas": {
      "CreateBtcInvoiceRequest": {
        "type": "object",
        "required": ["amount_sats"],
        "properties": {
          "chain": {
            "type": "string",
            "enum": ["btc"],
            "default": "btc",
            "description": "Optional; defaults to btc."
          },
          "amount_sats": {
            "type": "integer",
            "minimum": 1,
            "description": "Amount in satoshis (positive integer)."
          },
          "memo": {
            "type": "string",
            "maxLength": 200,
            "description": "Optional label, surfaced in the BIP-21 QR URI."
          },
          "expires_in": {
            "type": "integer",
            "minimum": 1,
            "description": "Optional invoice TTL in seconds."
          }
        }
      },
      "CreateBaseInvoiceRequest": {
        "type": "object",
        "required": ["chain", "amount_usd"],
        "properties": {
          "chain": {
            "type": "string",
            "enum": ["base"],
            "description": "Use base for USDC/USDT on Base."
          },
          "amount_usd": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Amount in USD."
          },
          "asset": {
            "type": "string",
            "enum": ["usdc", "usdt"],
            "default": "usdc",
            "description": "Optional; defaults to usdc. usdt is served in fixed-address mode."
          },
          "expires_in": {
            "type": "integer",
            "minimum": 1,
            "description": "Optional invoice TTL in seconds (xpub mode)."
          }
        }
      },
      "Invoice": {
        "type": "object",
        "description": "Invoice. EVM stablecoin invoices add amount_usd / amount_usdc / amount_usdc_units / qr_uri; fixed-address invoices also add mode, nonce, asset, and token_address.",
        "properties": {
          "invoice_id": { "type": "string" },
          "merchant_id": { "type": "string" },
          "chain": { "type": "string", "enum": ["btc", "base", "polygon", "eth"] },
          "asset": { "type": "string", "examples": ["BTC", "USDC", "USDT"] },
          "amount_btc": { "type": "string", "description": "Decimal BTC amount (BTC invoices)." },
          "receive_address": { "type": "string", "description": "On-chain address the payer sends to." },
          "child_index": { "type": ["integer", "null"], "description": "BIP-32 child index for xpub-derived addresses; null for fixed-address." },
          "status": { "$ref": "#/components/schemas/InvoiceStatus" },
          "tx_hash": { "type": ["string", "null"] },
          "paid_at": { "type": ["string", "null"], "format": "date-time" },
          "expires_at": { "type": "string", "format": "date-time" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" },
          "amount_usd": { "type": "number", "description": "USD amount (Base invoices)." },
          "amount_usdc": { "type": "string", "description": "Decimal stablecoin amount (Base invoices)." },
          "amount_usdc_units": { "type": "integer", "description": "Stablecoin amount in 6-decimal base units (Base invoices)." },
          "derivation_path": { "type": "string", "description": "Derivation path for xpub-derived addresses." },
          "network": { "type": "string", "description": "mainnet/testnet (BTC invoices)." },
          "amount_sats": { "type": "integer", "description": "Satoshi amount (BTC invoices)." },
          "qr_uri": { "type": "string", "description": "BIP-21 (BTC) or EIP-681 (Base) payment URI for QR rendering." },
          "verify_url": { "type": "string", "format": "uri", "description": "Block-explorer link for the receive address." },
          "mode": { "type": "string", "enum": ["fixed-address"], "description": "Present only for fixed-address invoices." },
          "nonce": { "type": "integer", "description": "Per-invoice nonce encoded in the amount's low decimals (fixed-address mode)." },
          "token_address": { "type": "string", "description": "ERC-20 token contract the payer must send (fixed-address mode)." }
        }
      },
      "InvoiceStatus": {
        "type": "string",
        "enum": ["pending", "partial", "confirmed", "expired", "failed"]
      },
      "Health": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" },
          "ws_connected": { "type": "boolean" },
          "subscribed_count": { "type": "integer" },
          "last_event_at": { "type": ["string", "null"], "format": "date-time" },
          "last_block_height": { "type": ["integer", "null"] },
          "uptime_s": { "type": "number" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "string", "examples": ["invalid_amount", "unsupported_chain", "chain_disabled", "asset_disabled", "unauthorized", "rate_limited", "capacity", "create_failed", "not_found"] },
              "message": { "type": "string" }
            },
            "required": ["code"]
          }
        }
      }
    }
  },
  "webhooks": {
    "paymentConfirmed": {
      "post": {
        "operationId": "paymentWebhook",
        "summary": "Payment confirmation webhook",
        "description": "When a payment confirms, the listener POSTs a JSON event to the merchant's MERCHANT_WEBHOOK_URL. Verify by recomputing HMAC-SHA256(ZETTAPAY_WEBHOOK_SECRET, rawBody) and comparing in constant time against X-ZettaPay-Signature. Deliveries retry up to 10 times on a Stripe-grade backoff curve.",
        "parameters": [
          { "name": "X-ZettaPay-Signature", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Hex HMAC-SHA256 of the raw body." },
          { "name": "X-ZettaPay-Timestamp", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Delivery time, unix milliseconds." },
          { "name": "X-ZettaPay-Event-Id", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Stable event id for idempotency." },
          { "name": "X-ZettaPay-Attempt", "in": "header", "required": true, "schema": { "type": "string" }, "description": "1-indexed attempt number." }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "invoice_id": { "type": "string" },
                  "status": { "$ref": "#/components/schemas/InvoiceStatus" },
                  "tx_hash": { "type": "string" },
                  "chain": { "type": "string" },
                  "asset": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Acknowledged. Any non-2xx (or timeout) schedules a retry." }
        }
      }
    }
  }
}
