{
  "openapi": "3.0.3",
  "info": {
    "title": "Hypawave Agent API",
    "version": "1.0.0",
    "description": "Settlement-triggered execution API for AI agents. Hypawave enables agents to create payment requests, verify settlement, and trigger deterministic state transitions over the Bitcoin Lightning Network. Payment is the authorization — confirmed settlement unconditionally unlocks access to encrypted data.\n\n**Authentication paths:** Path 2 uses a dashboard-issued API key (`sk_test_*` / `sk_live_*`) provisioned by a human operator at https://hypawave.com — there is no agent-callable endpoint that mints API keys. Paths 3a/3b support accountless operation with a secp256k1 keypair; no account, SDK, or onboarding is required, and identity is auto-created on the first signed request.\n\n**Preimage requirement:** All settlement confirmation endpoints require the payer to submit the Lightning payment preimage. This proves the payment was made. **Do not pay unless your wallet returns a preimage — files and data will not unlock without it.** Use any programmable Lightning infrastructure (LND, CLN, Alby API, LNbits, NWC, etc.) that exposes the preimage after payment. Consumer wallets (Wallet of Satoshi, Phoenix, etc.) do not reliably expose the preimage and are not suitable."
  },
  "servers": [
    {
      "url": "https://hypawave.com",
      "description": "Hypawave API"
    }
  ],
  "tags": [
    { "name": "Invoices", "description": "Create and list payment requests with settlement-triggered execution semantics" },
    { "name": "Offers", "description": "Persistent payment offers with accountless pubkey-signature auth. Agents create offers, payers spawn payment intents with fresh Lightning invoices. Settlement triggers fee deduction and webhook execution." },
    { "name": "Files", "description": "Upload encrypted files and store encryption keys for settlement-gated delivery" },
    { "name": "Balance", "description": "Check agent balance and settle outstanding fees" },
    { "name": "Keys", "description": "API key management (session-authenticated)" },
    { "name": "Settlement", "description": "Verify settlement status and retrieve deterministically unlocked encryption keys" },
    { "name": "Identity", "description": "Identity registration for accountless onboarding" }
  ],
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Agent API key in the format sk_(test|live)_<base64url>. Keys are issued only through the Hypawave dashboard (session + CSRF authenticated) — `/api/agent/create-key` is not callable with a Bearer API key or pubkey signature. A human operator must provision the key and supply it to the agent. For fully autonomous onboarding with no human in the loop, use Path 3a or 3b."
      },
      "PubkeySignature": {
        "type": "apiKey",
        "in": "header",
        "name": "x-pubkey",
        "description": "Accountless pubkey-signature authentication with replay protection. Requires five headers: `x-pubkey` (hex-encoded secp256k1 compressed public key), `x-signature` (hex-encoded signature over the canonical hash), `x-signed-payload-hash` (hex-encoded SHA-256 of the request body; use SHA-256 of empty string for GET), `x-timestamp` (Unix timestamp in seconds), and `x-nonce` (unique random string, 8–128 chars). Canonical signed message = SHA-256(x-signed-payload-hash + ':' + x-timestamp + ':' + x-nonce). Timestamp tolerance: 300 seconds. Each nonce is single-use."
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" },
          "message": { "type": "string" }
        }
      },
      "ValidationErrorResponse": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "enum": ["validation_error"] },
          "message": { "type": "string" },
          "field": { "type": "string" }
        }
      },
      "AuthErrorResponse": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "enum": ["missing_auth", "invalid_key_format", "invalid_api_key", "api_key_revoked"]
          },
          "message": { "type": "string" }
        }
      },
      "CreateInvoiceRequest": {
        "type": "object",
        "required": ["client_email", "client_first_name", "client_last_name", "amount", "due_date"],
        "properties": {
          "client_email": {
            "type": "string",
            "format": "email",
            "description": "Email address of the payer or receiving agent"
          },
          "client_first_name": {
            "type": "string",
            "minLength": 1,
            "description": "First name of the payer or receiving agent"
          },
          "client_last_name": {
            "type": "string",
            "minLength": 1,
            "description": "Last name of the payer or receiving agent"
          },
          "company_name": {
            "type": "string",
            "nullable": true,
            "description": "Company name (optional)"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Payment request description or memo (optional)"
          },
          "amount": {
            "type": "number",
            "description": "Payment amount in the specified currency. Min and max limits are configurable — query GET /api/public-settings for current values (min_invoice_usd, max_invoice_usd)."
          },
          "currency": {
            "type": "string",
            "default": "USD",
            "description": "ISO 4217 fiat currency code. 40+ currencies supported including USD, EUR, GBP, JPY, etc."
          },
          "due_date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Due date in YYYY-MM-DD format"
          },
          "creator_fingerprint": {
            "type": "string",
            "nullable": true,
            "description": "Optional device/agent fingerprint for tracking"
          },
          "expires_in": {
            "type": "string",
            "nullable": true,
            "enum": ["1h", "24h", "7d"],
            "description": "Payment request expiration duration. null means no expiration."
          },
          "creator_first_name": {
            "type": "string",
            "nullable": true,
            "description": "Override creator first name (defaults to user profile)"
          },
          "creator_last_name": {
            "type": "string",
            "nullable": true,
            "description": "Override creator last name (defaults to user profile)"
          },
          "creator_company_name": {
            "type": "string",
            "nullable": true,
            "description": "Override creator company name (defaults to user profile)"
          },
          "payment_destination": {
            "type": "string",
            "description": "Optional override for the Lightning payment destination (Lightning Address or LNURL-pay URL). Defaults to the API key owner's stored lightning_address. Override supports marketplace routing (different seller per invoice), multi-wallet owners, and pass-through flows where the agent facilitates for a third-party creator. Funds always flow wallet-to-wallet — Hypawave never takes custody."
          },
          "execution_webhook": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Optional HTTPS URL. Hypawave will POST a settlement notification to this URL immediately when the payer confirms payment via /api/invoice/{id}/confirm. Payload: {invoice_id, stream_id, payment_hash, preimage, amount_sats, settled_at}. Fire-and-forget (5s timeout, no retries). Enables real-time creator-side notification without polling."
          }
        }
      },
      "CreateInvoiceResponse": {
        "type": "object",
        "required": ["ok", "invoice_id", "access_token", "payment_url", "stream_id", "amount", "currency", "sats", "btc_usd_rate"],
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "invoice_id": { "type": "integer", "description": "Unique payment request identifier" },
          "access_token": { "type": "string", "description": "Token for accessing the payment page" },
          "payment_url": { "type": "string", "description": "Browser-based payment page. Send to a payer that pays via a browser. For agent-to-agent payments, use the programmatic flow (paystream-cb → pay → confirm)." },
          "instructions_url": { "type": "string", "format": "uri", "description": "URL to machine-readable settlement instructions (llms.txt). Forward to payer agents so they can complete the payment flow without prior Hypawave knowledge." },
          "stream_id": { "type": "string", "format": "uuid", "description": "UUID of the associated Hypawave Protocol stream" },
          "amount": { "type": "number", "description": "Payment amount in the specified currency" },
          "currency": { "type": "string", "description": "Currency code used for the payment request" },
          "sats": { "type": "integer", "description": "Amount in satoshis at the locked BTC price" },
          "btc_usd_rate": { "type": "number", "description": "BTC/fiat price used for conversion" },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "ISO 8601 expiration timestamp, or null if no expiration"
          }
        }
      },
      "InvoiceActivation": {
        "type": "object",
        "description": "Upfront activation fee bolt11 that the accountless creator must pay before the invoice becomes payable by a payer. Earned at invoice creation; no refunds. Payer-facing routes respond with 402 activation_pending until status='active'.",
        "required": ["fee_bolt11", "fee_payment_hash", "fee_amount_sats", "expires_at", "status"],
        "properties": {
          "fee_bolt11": { "type": "string", "description": "BOLT11 invoice the creator must pay to activate the payment request. Minted by Hypawave via LNbits Cloud." },
          "fee_payment_hash": { "type": "string", "description": "Hex-encoded payment hash of the fee bolt11 (64 chars)." },
          "fee_amount_sats": { "type": "integer", "description": "Fee amount in satoshis. Computed server-side from the declared amount and current fee policy — query GET /api/public-settings for fee_percent." },
          "expires_at": { "type": "string", "format": "date-time", "description": "ISO 8601 expiration of the fee bolt11. If the creator does not settle the fee before this timestamp, the invoice is lazy-expired on next access and becomes unusable." },
          "status": { "type": "string", "enum": ["pending"], "description": "Always 'pending' on creation. Transitions to 'active' when the fee bolt11 settles (via LNbits webhook or polling fallback), or 'expired' if not settled before expires_at." }
        }
      },
      "PaymentPayload": {
        "type": "object",
        "description": "Self-describing payment payload for agent-to-agent interop. The creator constructs this from the create-invoice response and sends it to the receiver agent. The receiver needs no SDK, no account, and no prior Hypawave knowledge — just three HTTP calls. Construct `bolt11_url` as `https://hypawave.com/api/paystream-cb?token={access_token}`, `confirm_url` as `https://hypawave.com/api/invoice/{invoice_id}/confirm`, and `spec_url` as `https://hypawave.com/.well-known/openapi.json`.",
        "required": ["protocol", "invoice_id", "amount", "currency", "sats", "bolt11_url", "confirm_url", "payment_url"],
        "properties": {
          "protocol": { "type": "string", "enum": ["hypawave/v1"], "description": "Protocol identifier and version" },
          "invoice_id": { "type": "integer", "description": "Invoice identifier for the confirm endpoint" },
          "amount": { "type": "number", "description": "Payment amount in the specified currency" },
          "currency": { "type": "string", "description": "Currency code" },
          "sats": { "type": "integer", "description": "Amount in satoshis" },
          "btc_usd_rate": { "type": "number", "description": "BTC/fiat rate used for conversion" },
          "description": { "type": "string", "nullable": true, "description": "Human-readable description of what the payment is for" },
          "expires_at": { "type": "string", "format": "date-time", "nullable": true, "description": "ISO 8601 expiration timestamp" },
          "bolt11_url": { "type": "string", "format": "uri", "description": "GET this URL to fetch the Lightning bolt11 invoice to pay" },
          "confirm_url": { "type": "string", "format": "uri", "description": "POST {payment_hash, preimage} to this URL after paying to confirm settlement" },
          "payment_url": { "type": "string", "format": "uri", "description": "Browser-based payment page. Send to a payer that pays via a browser. For agent-to-agent payments, use bolt11_url and confirm_url." },
          "spec_url": { "type": "string", "format": "uri", "description": "OpenAPI spec URL for full API reference (fallback)" },
          "instructions_url": { "type": "string", "format": "uri", "description": "URL to machine-readable settlement instructions (llms.txt). Payer agents can read this to understand the full payment flow." }
        },
        "example": {
          "protocol": "hypawave/v1",
          "invoice_id": 42,
          "amount": 5.00,
          "currency": "USD",
          "sats": 5000,
          "btc_usd_rate": 100000,
          "description": "API access",
          "expires_at": "2026-05-01T00:00:00Z",
          "bolt11_url": "https://hypawave.com/api/paystream-cb?token=abc123",
          "confirm_url": "https://hypawave.com/api/invoice/42/confirm",
          "payment_url": "https://hypawave.com/client_payment?invoiceId=42&token=abc123",
          "spec_url": "https://hypawave.com/.well-known/openapi.json",
          "instructions_url": "https://hypawave.com/llms.txt"
        }
      },
      "CreateOfferInvoiceResponse": {
        "description": "Accountless (Path 3a) create-invoice response. Extends CreateInvoiceResponse with a top-level activation sibling containing the upfront fee bolt11 the creator must pay before the invoice becomes payable.",
        "allOf": [
          { "$ref": "#/components/schemas/CreateInvoiceResponse" },
          {
            "type": "object",
            "required": ["activation"],
            "properties": {
              "activation": { "$ref": "#/components/schemas/InvoiceActivation" }
            }
          }
        ]
      },
      "BalanceResponse": {
        "type": "object",
        "required": ["available_sats", "fiat_equivalent", "currency", "btc_price", "has_outstanding_fee"],
        "properties": {
          "available_sats": { "type": "integer", "description": "Available balance in satoshis (can be negative if settlement fees outstanding)" },
          "fiat_equivalent": { "type": "number", "description": "Fiat equivalent of the balance at current BTC price" },
          "currency": { "type": "string", "description": "Currency code used for fiat_equivalent" },
          "btc_price": { "type": "number", "description": "Current BTC price in the requested currency" },
          "has_outstanding_fee": { "type": "boolean", "description": "True if balance is negative (outstanding settlement fees — blocks new payment requests, never blocks delivery)" }
        }
      },
      "TopupRequest": {
        "type": "object",
        "description": "Empty body. Top-ups are fee-settlement only — the server always mints a bolt11 for exactly the owed amount (= -balance.available_sats when negative). Any fields in the body are ignored.",
        "additionalProperties": true
      },
      "TopupResponse": {
        "type": "object",
        "required": ["ok", "bolt11", "payment_hash", "amount_sats"],
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "bolt11": { "type": "string", "description": "BOLT11 Lightning invoice to pay for the fee settlement" },
          "payment_hash": { "type": "string", "description": "Payment hash of the Lightning invoice" },
          "amount_sats": { "type": "integer", "description": "Exact owed amount in satoshis (= -available_sats)" },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "ISO 8601 expiration of the Lightning invoice"
          },
          "reused": {
            "type": "boolean",
            "description": "True if an existing pending top-up was returned instead of a fresh one (double-click protection)."
          }
        }
      },
      "CreateKeyRequest": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": ["test", "live"],
            "description": "Key mode. 'test' keys for development, 'live' for production."
          },
          "label": {
            "type": "string",
            "maxLength": 100,
            "description": "Optional human-readable label for the key"
          }
        }
      },
      "CreateKeyResponse": {
        "type": "object",
        "required": ["key", "prefix", "mode", "message"],
        "properties": {
          "key": { "type": "string", "description": "Full API key (sk_test_* or sk_live_*). Store securely — shown only once." },
          "prefix": { "type": "string", "description": "First 10 characters of the key for identification" },
          "mode": { "type": "string", "enum": ["test", "live"] },
          "message": { "type": "string" }
        }
      },
      "RevokeKeyRequest": {
        "type": "object",
        "required": ["key_id"],
        "properties": {
          "key_id": { "type": "string", "description": "UUID of the key to revoke" },
          "reason": {
            "type": "string",
            "maxLength": 200,
            "description": "Optional reason for revocation"
          }
        }
      },
      "RevokeKeyResponse": {
        "type": "object",
        "required": ["ok"],
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "revoked": { "type": "boolean", "description": "True if key was revoked by this request" },
          "already_revoked": { "type": "boolean", "description": "True if key was already revoked" }
        }
      },
      "ListKeysResponse": {
        "type": "object",
        "required": ["keys"],
        "properties": {
          "keys": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["id", "key_prefix", "mode", "created_at"],
              "properties": {
                "id": { "type": "string", "description": "Key UUID" },
                "key_prefix": { "type": "string", "description": "First characters of the key for identification" },
                "mode": { "type": "string", "enum": ["test", "live"] },
                "label": { "type": "string", "nullable": true },
                "created_at": { "type": "string", "format": "date-time" },
                "revoked_at": { "type": "string", "format": "date-time", "nullable": true },
                "last_used_at": { "type": "string", "format": "date-time", "nullable": true }
              }
            }
          }
        }
      },
      "UnlockStatusRequest": {
        "type": "object",
        "required": ["invoice_ids"],
        "properties": {
          "invoice_ids": {
            "type": "array",
            "items": { "type": "integer" },
            "maxItems": 25,
            "description": "Array of payment request IDs to check settlement status (max 25)"
          }
        }
      },
      "UnlockStatusResponse": {
        "type": "object",
        "required": ["unlocked"],
        "properties": {
          "unlocked": {
            "type": "object",
            "additionalProperties": { "type": "boolean" },
            "description": "Map of invoice_id to unlock status (only includes settled and unlocked requests)"
          },
          "statuses": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "unlocked": { "type": "boolean" },
                "status": { "type": "string" },
                "failure_reason": { "type": "string", "nullable": true },
                "settlement_proof": {
                  "type": "object",
                  "nullable": true,
                  "description": "Cryptographic settlement proof from the Lightning Network. Null if payment has not settled.",
                  "properties": {
                    "payment_hash": { "type": "string", "nullable": true, "description": "SHA-256 hash of the preimage — unique identifier for the Lightning payment" },
                    "preimage": { "type": "string", "nullable": true, "description": "Cryptographic proof of payment. SHA-256(preimage) === payment_hash. Possession of this value proves settlement occurred." },
                    "settled_at": { "type": "string", "format": "date-time", "nullable": true, "description": "ISO 8601 timestamp of when settlement was confirmed" }
                  }
                }
              }
            },
            "description": "Detailed settlement status for each payment request including stream state and cryptographic proof"
          }
        }
      },
      "GetKeyResponse": {
        "type": "object",
        "required": ["encryption_key", "iv_hex", "downloads_used", "max_downloads", "reclaim_window_hours"],
        "properties": {
          "encryption_key": { "type": "string", "description": "Base64-encoded AES-256-GCM encryption key — released deterministically upon settlement confirmation" },
          "iv_hex": { "type": "string", "description": "Hex-encoded initialization vector for AES-256-GCM decryption" },
          "downloads_used": { "type": "integer", "description": "Number of download claims used so far" },
          "max_downloads": { "type": "integer", "description": "Maximum download claims allowed" },
          "reclaim_window_hours": { "type": "integer", "description": "Hours from first download during which reclaims are allowed (typically 72)" }
        }
      },
      "UploadUrlRequest": {
        "type": "object",
        "required": ["fileName", "contentType"],
        "properties": {
          "fileName": { "type": "string", "description": "Original file name" },
          "contentType": {
            "type": "string",
            "description": "MIME type of the file. Allowed: application/pdf, application/zip, image/jpeg, image/png, image/gif, image/webp, video/mp4, video/webm, audio/mpeg, audio/wav, text/plain, application/octet-stream"
          },
          "fileSize": { "type": "integer", "description": "File size in bytes (optional, validated against server max)" }
        }
      },
      "UploadUrlResponse": {
        "type": "object",
        "required": ["signedUrl", "objectKey"],
        "properties": {
          "signedUrl": { "type": "string", "description": "Presigned URL for direct file upload via PUT request (expires in 1 hour)" },
          "objectKey": { "type": "string", "description": "Object key in storage — use as encrypted_file_url in store-file" }
        }
      },
      "StoreFileRequest": {
        "type": "object",
        "required": ["invoice_id", "file_name", "encrypted_file_url", "iv_hex"],
        "properties": {
          "invoice_id": { "type": "integer", "description": "ID of the invoice to attach the file to" },
          "file_name": { "type": "string", "description": "Original file name for display" },
          "encrypted_file_url": { "type": "string", "description": "Object key from upload-url response" },
          "iv_hex": { "type": "string", "description": "Hex-encoded initialization vector used for AES-256-GCM encryption" },
          "key_hash": { "type": "string", "description": "SHA-256 hash of the encryption key (optional)" },
          "size": { "type": "integer", "description": "File size in bytes (optional)" }
        }
      },
      "StoreFileResponse": {
        "type": "object",
        "required": ["ok", "id"],
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "id": { "type": "string", "description": "UUID of the stored file record — use as invoice_file_id in store-file-key" }
        }
      },
      "StoreFileKeyRequest": {
        "type": "object",
        "required": ["invoice_file_id", "key_b64"],
        "properties": {
          "invoice_file_id": { "type": "string", "description": "UUID of the file record from store-file response" },
          "key_b64": { "type": "string", "description": "Base64-encoded AES-256-GCM encryption key" }
        }
      },
      "ListInvoicesResponse": {
        "type": "object",
        "required": ["invoices", "has_more"],
        "properties": {
          "invoices": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "invoice_id": { "type": "integer" },
                "stream_id": { "type": "string", "nullable": true },
                "amount": { "type": "number" },
                "currency": { "type": "string", "description": "Currency code (e.g. USD)" },
                "description": { "type": "string", "nullable": true },
                "status": { "type": "string", "enum": ["paid", "pending", "expired"] },
                "payment_destination": { "type": "string", "nullable": true, "description": "Creator's Lightning Address or LNURL-pay URL" },
                "access_token": { "type": "string", "nullable": true, "description": "Token for payment flow — use with /api/paystream-cb" },
                "payment_url": { "type": "string", "nullable": true, "description": "Browser-based payment page. Send to a payer that pays via a browser." },
                "instructions_url": { "type": "string", "format": "uri", "description": "URL to machine-readable settlement instructions (llms.txt)" },
                "client_email": { "type": "string", "nullable": true, "description": "Payer's email address" },
                "client_first_name": { "type": "string", "nullable": true, "description": "Payer's first name" },
                "client_last_name": { "type": "string", "nullable": true, "description": "Payer's last name" },
                "created_at": { "type": "string", "format": "date-time" },
                "due_date": { "type": "string" },
                "expires_at": { "type": "string", "format": "date-time", "nullable": true },
                "has_file": { "type": "boolean" },
                "payment_hash": { "type": "string", "nullable": true },
                "preimage": { "type": "string", "nullable": true },
                "paid_at": { "type": "string", "format": "date-time", "nullable": true }
              }
            }
          },
          "has_more": { "type": "boolean", "description": "True if more results are available beyond the current page" }
        }
      },
      "CreateOfferRequest": {
        "type": "object",
        "required": ["amount", "pricing_type", "description", "payment_destination", "signed_payload_hash", "signature"],
        "properties": {
          "amount": {
            "type": "number",
            "minimum": 0,
            "exclusiveMinimum": true,
            "description": "Payment amount in the specified currency (sats or fiat)"
          },
          "pricing_type": {
            "type": "string",
            "enum": ["sats", "fiat"],
            "description": "Pricing mode. 'sats' for fixed satoshi amount, 'fiat' for fiat-denominated (converted to sats at payment time)"
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 fiat currency code (e.g. 'USD', 'EUR'). Required when pricing_type is 'fiat'. Ignored when pricing_type is 'sats'."
          },
          "description": {
            "type": "string",
            "minLength": 1,
            "description": "Human-readable description of the offer"
          },
          "payment_destination": {
            "type": "string",
            "minLength": 1,
            "description": "Creator's always-available Lightning receiving endpoint. Accepts Lightning Address (user@domain.com) or LNURL-pay HTTPS URL. Payers pay this destination directly — Hypawave never receives principal funds. Required."
          },
          "execution_webhook": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "HTTPS URL called upon successful settlement. Receives payment proof payload. Optional."
          },
          "metadata": {
            "type": "object",
            "nullable": true,
            "description": "Arbitrary JSON metadata attached to the offer (optional)"
          },
          "activation_window": {
            "oneOf": [
              { "type": "string", "pattern": "^\\d+d$" },
              { "type": "integer", "minimum": 1 }
            ],
            "nullable": true,
            "description": "Accountless creators only. Duration the offer stays payable after activation. Accepts `\"Nd\"` strings (e.g. `\"30d\"`) or positive integer days. Default `\"30d\"`. Bounds `[1d, 365d]`. Ignored for postpaid (Bearer API key) creators. Invalid input → `400 invalid_activation_window`."
          },
          "signed_payload_hash": {
            "type": "string",
            "description": "Hex-encoded SHA-256 hash of the canonical JSON request body. Must match the computed hash of the raw body."
          },
          "signature": {
            "type": "string",
            "description": "Hex-encoded secp256k1 signature over the signed_payload_hash"
          },
          "sig_algo": {
            "type": "string",
            "default": "secp256k1-sha256-v1",
            "description": "Signature algorithm identifier (default: secp256k1-sha256-v1)"
          }
        }
      },
      "OfferActivation": {
        "type": "object",
        "required": ["fee_bolt11", "fee_payment_hash", "fee_amount_sats", "expires_at", "status", "requested_window_ms", "terms_hash"],
        "description": "Top-level sibling on `POST /api/offers` and `POST /api/offers/{id}/renew` responses (accountless creators only). The creator must pay `fee_bolt11` before the offer becomes payable. Postpaid (Bearer API key) creators do not receive this sibling.",
        "properties": {
          "fee_bolt11": { "type": "string", "description": "BOLT11 Lightning invoice for the activation fee. Issued by Hypawave via LNbits Cloud." },
          "fee_payment_hash": { "type": "string", "description": "SHA-256 hash extracted from the fee bolt11. Binds settlement to this activation row." },
          "fee_amount_sats": { "type": "integer", "description": "Activation fee amount in satoshis. Computed server-side from the declared price and current fee policy — query GET /api/public-settings for fee_percent." },
          "expires_at": { "type": "string", "format": "date-time", "nullable": true, "description": "ISO 8601 expiration of the fee bolt11. If unpaid past this point, call `/renew` to mint a fresh one." },
          "status": { "type": "string", "enum": ["pending"], "description": "Always `pending` at creation. Transitions to `active` on webhook settlement." },
          "requested_window_ms": { "type": "integer", "description": "Requested activation window in milliseconds. On settlement, `window_start = settled_at`, `window_end = settled_at + requested_window_ms`." },
          "terms_hash": { "type": "string", "description": "Hex SHA-256 of the canonical serialization of `{amount, currency, pricing_type, description, payment_destination (trimmed), declared_price_sats}`. Recomputed at pay time — mismatch → `409 terms_changed`." }
        }
      },
      "CreateOfferResponse": {
        "type": "object",
        "required": ["offer_id", "created_at"],
        "properties": {
          "offer_id": { "type": "string", "format": "uuid", "description": "Unique identifier of the created offer" },
          "created_at": { "type": "string", "format": "date-time", "description": "ISO 8601 creation timestamp" },
          "max_payments": { "type": "integer", "nullable": true, "description": "Maximum number of payments allowed (null = unlimited)" },
          "billing_model": {
            "type": "string",
            "enum": ["postpaid", "accountless_activation"],
            "description": "Settlement model. `accountless_activation` for pubkey-auth creators; `postpaid` for Bearer-token creators."
          },
          "activation": {
            "$ref": "#/components/schemas/OfferActivation",
            "description": "Present only for `billing_model=accountless_activation`. Absent for postpaid."
          }
        }
      },
      "RenewOfferRequest": {
        "type": "object",
        "description": "Optional body for offer renewal. Request is pubkey-signed by the creator (headers: x-pubkey, x-signature, x-signed-payload-hash, x-timestamp, x-nonce).",
        "properties": {
          "activation_window": {
            "oneOf": [
              { "type": "string", "pattern": "^\\d+d$" },
              { "type": "integer", "minimum": 1 }
            ],
            "nullable": true,
            "description": "Duration to extend service after settlement. Accepts `\"Nd\"` strings or positive integer days. Default `\"30d\"`. Bounds `[1d, 365d]`."
          }
        }
      },
      "RenewOfferResponse": {
        "type": "object",
        "required": ["ok", "offer_id", "activation"],
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "offer_id": { "type": "string", "format": "uuid" },
          "reused": { "type": "boolean", "description": "True if an existing unexpired pending activation row was returned unchanged (retry-safe)." },
          "activation": { "$ref": "#/components/schemas/OfferActivation" }
        }
      },
      "GetOfferResponse": {
        "type": "object",
        "required": ["id", "amount", "currency", "pricing_type", "description", "creator_pubkey", "created_at"],
        "properties": {
          "id": { "type": "string", "format": "uuid", "description": "Offer UUID" },
          "amount": { "type": "number", "description": "Payment amount in the specified currency" },
          "currency": { "type": "string", "description": "Currency code ('sats' or ISO 4217 fiat code)" },
          "pricing_type": { "type": "string", "enum": ["sats", "fiat"], "description": "Pricing mode" },
          "description": { "type": "string", "description": "Human-readable offer description" },
          "creator_pubkey": { "type": "string", "description": "Hex-encoded secp256k1 public key of the offer creator" },
          "created_at": { "type": "string", "format": "date-time", "description": "ISO 8601 creation timestamp" },
          "max_payments": { "type": "integer", "nullable": true, "description": "Maximum number of payments allowed (null = unlimited)" },
          "payment_count": { "type": "integer", "description": "Number of settled payments so far" },
          "status": { "type": "string", "enum": ["active", "deactivated", "exhausted"], "description": "Offer status" },
          "has_files": { "type": "boolean", "description": "Whether files are attached to this offer" },
          "file_count": { "type": "integer", "description": "Number of files attached" },
          "billing_model": {
            "type": "string",
            "enum": ["postpaid", "accountless_activation"],
            "readOnly": true,
            "description": "Settlement model. `accountless_activation` offers enforce the upfront activation gate at `/api/offers/{id}/pay`."
          },
          "activation_window_end": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "readOnly": true,
            "description": "ISO 8601 end of the current activation window. Present when an active `offer_activations` row exists. Payers can infer offer payability by comparing against `now()`; once elapsed, the offer requires renewal."
          },
          "instructions_url": { "type": "string", "format": "uri", "description": "URL to machine-readable settlement instructions (llms.txt)" }
        }
      },
      "PayOfferRequest": {
        "type": "object",
        "properties": {
          "payer_pubkey": {
            "type": "string",
            "nullable": true,
            "description": "Optional hex-encoded secp256k1 public key of the payer (for attribution)"
          }
        }
      },
      "PayOfferResponse": {
        "type": "object",
        "required": ["payment_intent_id", "bolt11", "payment_hash", "locked_amount_sats", "locked_currency", "payer_secret"],
        "properties": {
          "payment_intent_id": { "type": "string", "format": "uuid", "description": "Unique identifier of the spawned payment intent" },
          "bolt11": { "type": "string", "description": "BOLT11 Lightning invoice issued by the creator's receiving endpoint. Pay this directly — Hypawave does not receive the principal." },
          "payment_hash": { "type": "string", "description": "Payment hash extracted from the creator-issued bolt11" },
          "locked_amount_sats": { "type": "integer", "description": "Amount in satoshis locked at creation time" },
          "locked_currency": { "type": "string", "description": "Currency the amount was locked in ('sats' or fiat code)" },
          "locked_btc_rate": { "type": "number", "nullable": true, "description": "BTC/fiat price used for conversion. Null if pricing_type is 'sats'." },
          "expires_at": { "type": "string", "format": "date-time", "nullable": true, "description": "ISO 8601 expiration. Always null for direct-pay offers — rely on the expiry encoded in the bolt11 itself." },
          "payer_secret": { "type": "string", "description": "Single-use secret (hex) the payer must retain. Required to query status via GET /payment-intent/{id}/status and to confirm settlement via POST /payment-intent/{id}/confirm." },
          "instructions_url": { "type": "string", "format": "uri", "description": "URL to machine-readable settlement instructions (llms.txt)" }
        }
      },
      "ConfirmPaymentIntentRequest": {
        "type": "object",
        "required": ["preimage", "payer_secret"],
        "properties": {
          "preimage": {
            "type": "string",
            "pattern": "^[0-9a-fA-F]{64}$",
            "description": "64-character hex Lightning preimage revealed by the payer's wallet on successful payment. Proof-of-payment: SHA-256(preimage) must equal the payment_hash stored on the intent."
          },
          "payer_secret": {
            "type": "string",
            "description": "The payer_secret returned by POST /offers/{id}/pay, used to authorize this confirmation."
          }
        }
      },
      "ConfirmPaymentIntentResponse": {
        "type": "object",
        "required": ["ok", "payment_intent_id"],
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "payment_intent_id": { "type": "string", "format": "uuid", "description": "UUID of the confirmed payment intent" },
          "already_settled": { "type": "boolean", "description": "True if this intent was already settled before this call (idempotent replay)" },
          "settled_unpaid_fee": { "type": "boolean", "description": "True if the intent settled but the creator's fee could not be deducted (execution webhook was NOT fired)" }
        }
      },
      "ListOffersResponse": {
        "type": "object",
        "required": ["offers", "count"],
        "properties": {
          "offers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string", "format": "uuid", "description": "Offer UUID" },
                "amount": { "type": "number", "description": "Payment amount" },
                "currency": { "type": "string", "description": "Currency code" },
                "pricing_type": { "type": "string", "enum": ["sats", "fiat"], "description": "Pricing mode" },
                "description": { "type": "string", "description": "Offer description" },
                "status": { "type": "string", "description": "Offer status: active | deactivated | exhausted" },
                "max_payments": { "type": "integer", "nullable": true, "description": "Maximum number of times the offer can be paid (null = unlimited)" },
                "payment_count": { "type": "integer", "description": "Number of times the offer has been paid" },
                "created_at": { "type": "string", "format": "date-time", "description": "ISO 8601 creation timestamp" },
                "execution_webhook": { "type": "string", "nullable": true, "description": "Webhook URL for settlement execution" }
              }
            }
          },
          "count": { "type": "integer", "description": "Total number of offers returned" }
        }
      },
      "ListPaymentsResponse": {
        "type": "object",
        "required": ["payments", "has_more"],
        "properties": {
          "payments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "payment_intent_id": { "type": "string", "format": "uuid", "description": "Payment intent UUID" },
                "offer_id": { "type": "string", "format": "uuid", "description": "UUID of the parent offer" },
                "status": { "type": "string", "description": "Payment intent status: pending | settled | expired | settled_unpaid_fee" },
                "locked_amount_sats": { "type": "integer", "description": "Amount locked in satoshis" },
                "locked_fiat_amount": { "type": "number", "nullable": true, "description": "Fiat-denominated amount at lock time" },
                "locked_currency": { "type": "string", "description": "Currency the amount was locked in" },
                "locked_btc_rate": { "type": "number", "nullable": true, "description": "BTC/fiat rate at lock time" },
                "offer_description": { "type": "string", "nullable": true, "description": "Snapshot of the offer description" },
                "offer_amount": { "type": "number", "nullable": true, "description": "Snapshot of the offer amount" },
                "offer_currency": { "type": "string", "nullable": true, "description": "Snapshot of the offer currency" },
                "payer_pubkey": { "type": "string", "nullable": true, "description": "Payer pubkey if provided at payment time" },
                "payment_hash": { "type": "string", "nullable": true, "description": "Lightning payment hash" },
                "preimage": { "type": "string", "nullable": true, "description": "Lightning payment preimage (proof of payment)" },
                "receipt_verified": { "type": "boolean", "description": "True if SHA256(preimage) === payment_hash — cryptographic proof of settlement" },
                "settled_at": { "type": "string", "format": "date-time", "nullable": true, "description": "ISO 8601 settlement timestamp" },
                "created_at": { "type": "string", "format": "date-time", "description": "ISO 8601 creation timestamp" },
                "execution_fired": { "type": "boolean", "description": "True if the settlement webhook was triggered" }
              }
            }
          },
          "has_more": { "type": "boolean", "description": "True if more results are available beyond the current page" }
        }
      },
      "CreateOfferInvoiceRequest": {
        "type": "object",
        "required": ["client_email", "client_first_name", "client_last_name", "amount", "due_date", "payment_destination"],
        "properties": {
          "client_email": { "type": "string", "format": "email", "description": "Email address of the payer" },
          "client_first_name": { "type": "string", "minLength": 1, "description": "First name of the payer" },
          "client_last_name": { "type": "string", "minLength": 1, "description": "Last name of the payer" },
          "company_name": { "type": "string", "nullable": true, "description": "Company name (optional)" },
          "description": { "type": "string", "nullable": true, "description": "Invoice description or memo (optional)" },
          "amount": { "type": "number", "description": "Amount in the specified currency. Min and max limits are configurable — query GET /api/public-settings for current values (min_invoice_usd, max_invoice_usd)." },
          "currency": { "type": "string", "default": "USD", "description": "ISO 4217 fiat currency code" },
          "due_date": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "Due date in YYYY-MM-DD format" },
          "payment_destination": { "type": "string", "minLength": 1, "description": "Lightning Address (user@domain.com) or LNURL" },
          "expires_in": { "type": "string", "nullable": true, "enum": ["1h", "24h", "7d"], "description": "Invoice expiration duration" },
          "execution_webhook": { "type": "string", "format": "uri", "nullable": true, "description": "Optional HTTPS URL for real-time settlement notification. Same behavior as Agent API execution_webhook." }
        }
      },
      "StoreOfferFileRequest": {
        "type": "object",
        "required": ["offer_id", "storage_key", "filename", "size", "iv_hex"],
        "properties": {
          "offer_id": { "type": "string", "format": "uuid", "description": "UUID of the offer to attach the file to" },
          "storage_key": { "type": "string", "description": "Object key from upload-url response" },
          "filename": { "type": "string", "description": "Original file name for display" },
          "size": { "type": "integer", "description": "File size in bytes" },
          "content_type": { "type": "string", "nullable": true, "description": "MIME type of the file (optional)" },
          "iv_hex": { "type": "string", "description": "Hex-encoded initialization vector used for AES-256-GCM encryption" }
        }
      },
      "StoreOfferFileResponse": {
        "type": "object",
        "required": ["ok", "offer_file_id"],
        "properties": {
          "ok": { "type": "boolean", "enum": [true] },
          "offer_file_id": { "type": "string", "description": "UUID of the stored offer file record — use as offer_file_id in store-file-key" }
        }
      },
      "StoreOfferFileKeyRequest": {
        "type": "object",
        "required": ["offer_file_id", "wrapped_key"],
        "properties": {
          "offer_file_id": { "type": "string", "description": "UUID of the offer file record from store-file response" },
          "wrapped_key": { "type": "string", "description": "Base64-encoded wrapped AES-256-GCM encryption key" },
          "key_version": { "type": "integer", "default": 1, "description": "Key version (optional, default 1)" },
          "wrap_algo": { "type": "string", "default": "aes-256-gcm", "description": "Key wrapping algorithm (optional, default aes-256-gcm)" }
        }
      },
      "StoreOfferInvoiceFileRequest": {
        "type": "object",
        "required": ["invoice_id", "file_name", "encrypted_file_url", "iv_hex"],
        "properties": {
          "invoice_id": { "type": "integer", "description": "ID of the invoice to attach the file to" },
          "file_name": { "type": "string", "description": "Original file name for display" },
          "encrypted_file_url": { "type": "string", "description": "Object key from upload-url response" },
          "iv_hex": { "type": "string", "description": "Hex-encoded initialization vector used for AES-256-GCM encryption" },
          "key_hash": { "type": "string", "description": "SHA-256 hash of the encryption key (optional)" },
          "size": { "type": "integer", "description": "File size in bytes (optional)" }
        }
      },
      "InvoiceFileKeyRequest": {
        "type": "object",
        "required": ["invoice_file_id", "key_b64"],
        "properties": {
          "invoice_file_id": { "type": "string", "description": "UUID of the invoice file record from store-invoice-file response" },
          "key_b64": { "type": "string", "description": "Base64-encoded AES-256-GCM encryption key" }
        }
      },
      "OffersListInvoicesResponse": {
        "type": "object",
        "required": ["invoices", "has_more"],
        "properties": {
          "invoices": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "invoice_id": { "type": "integer" },
                "stream_id": { "type": "string", "nullable": true },
                "amount": { "type": "number" },
                "currency": { "type": "string", "description": "Fiat currency code" },
                "description": { "type": "string", "nullable": true },
                "status": { "type": "string", "enum": ["paid", "pending", "expired"] },
                "payment_destination": { "type": "string", "nullable": true, "description": "Creator's Lightning Address or LNURL-pay URL" },
                "access_token": { "type": "string", "nullable": true, "description": "Token for payment flow — use with /api/paystream-cb" },
                "payment_url": { "type": "string", "nullable": true, "description": "Browser-based payment page. Send to a payer that pays via a browser." },
                "instructions_url": { "type": "string", "format": "uri", "description": "URL to machine-readable settlement instructions (llms.txt)" },
                "client_email": { "type": "string", "nullable": true, "description": "Payer's email address" },
                "client_first_name": { "type": "string", "nullable": true },
                "client_last_name": { "type": "string", "nullable": true },
                "company_name": { "type": "string", "nullable": true },
                "created_at": { "type": "string", "format": "date-time" },
                "due_date": { "type": "string" },
                "expires_at": { "type": "string", "format": "date-time", "nullable": true },
                "has_file": { "type": "boolean" },
                "payment_hash": { "type": "string", "nullable": true },
                "preimage": { "type": "string", "nullable": true },
                "paid_at": { "type": "string", "format": "date-time", "nullable": true }
              }
            }
          },
          "has_more": { "type": "boolean", "description": "True if more results are available beyond the current page" }
        }
      }
    }
  },
  "paths": {
    "/api/offers": {
      "post": {
        "tags": ["Offers"],
        "summary": "Create a persistent payment offer",
        "description": "Creates a new persistent, non-custodial payment offer. Agents authenticate via pubkey signature headers or Bearer API key.\n\n**Upfront activation fee model (accountless creators):** Offers owned by accountless identities (pubkey-auth, no Supabase account) are created with `billing_model='accountless_activation'` and are inert until the creator pays an upfront activation fee. The 201 response includes a top-level `activation` sibling `{fee_bolt11, fee_payment_hash, fee_amount_sats, expires_at, status='pending', requested_window_ms, terms_hash}`. Pay `activation.fee_bolt11` to activate the offer — requires programmable Lightning infrastructure (LND, CLN, Alby API, LNbits, etc.) to pay the bolt11. Fee is computed server-side from the declared price (see GET /api/public-settings for fee_percent), with `declared_price_sats` snapshotted at mint time (fresh BTC conversion for fiat offers). Once settled (via LNbits webhook or polling fallback), the offer becomes payable for the requested `activation_window` duration, after which it can be extended via `POST /api/offers/{id}/renew`.\n\n**Postpaid (account-backed creators via Bearer API key):** legacy behavior — no activation fee, balance-debt model; `activation_window` is ignored.\n\nSupports two pricing modes: 'sats' (fixed satoshi amount) or 'fiat' (fiat-denominated, converted to sats at payment time via the `declared_price_sats` snapshot). The creator must supply a payment_destination (Lightning Address or LNURL-pay URL) where payers will send funds directly — Hypawave never receives the principal payment. The request body must be signed: compute SHA-256 of the body, sign with secp256k1, and include signed_payload_hash and signature in the body. Rate limited.",
        "operationId": "createOffer",
        "security": [{ "BearerAuth": [] }, { "PubkeySignature": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateOfferRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Offer created successfully",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreateOfferResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — validation_error (amount, pricing_type, currency, description, payment_destination, signed_payload_hash, or signature invalid), invalid_payment_destination, identity_missing_pubkey, invalid_activation_window (accountless only: unparseable/out-of-bounds activation_window)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed — missing or invalid pubkey signature headers, or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthErrorResponse" }
              }
            }
          },
          "402": {
            "description": "Payment required — outstanding_fee (postpaid creators only: balance is negative; call /api/topup or /api/agent/topup to settle). Does not apply to accountless offers (they pay an upfront activation fee instead).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error — offer_creation_failed, activation_persist_failed (accountless only; fee bolt11 minted but activation row insert failed)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "502": {
            "description": "Upstream failure (accountless only) — price_feed_unavailable (BTC price fetch failed for fiat offer), activation_creation_failed (LNbits mint failed), fee_invoice_malformed (minted bolt11 had no decodable payment_hash/expires_at)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/offers/{id}/renew": {
      "post": {
        "tags": ["Offers"],
        "summary": "Renew offer activation (accountless creator)",
        "description": "Mints a fresh activation fee bolt11 to extend or resume service on an accountless offer. Creator-only (pubkey must match the offer's `creator_pubkey`). The creator must pay the returned `fee_bolt11` using programmable Lightning infrastructure (LND, CLN, Alby API, LNbits, etc.). Response shape mirrors the `activation` sibling on `POST /api/offers`.\n\n**Behavior:**\n- If a pending activation row exists with an unexpired bolt11 → returns it unchanged with `reused: true` (double-click / retry safe).\n- If a pending row exists with an expired bolt11 → it is lazy-expired (`status='expired'`) and a fresh one is minted.\n- If the current active window is still running (`window_end > now()`) → returns `400 activation_not_needed` with the existing `window_end` so the caller can decide when to renew.\n- If the current window has elapsed → the row is lazy-expired and a fresh activation is minted.\n- If no live row exists → a fresh activation is minted.\n\n`declared_price_sats` is recomputed at renewal time (fresh BTC price for fiat offers), so renewing a fiat offer locks a fresh fee basis. `activation_window` defaults to `30d`, bounds `[1d, 365d]`.",
        "operationId": "renewOffer",
        "security": [{ "PubkeySignature": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "UUID of the offer to renew"
          },
          {
            "name": "x-pubkey",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 public key"
          },
          {
            "name": "x-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 signature"
          },
          {
            "name": "x-signed-payload-hash",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded SHA-256 hash of the request body"
          },
          {
            "name": "x-timestamp",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Unix timestamp (seconds)"
          },
          {
            "name": "x-nonce",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Unique nonce for this request"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RenewOfferRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Activation bolt11 (fresh or reused)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RenewOfferResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — invalid_activation_window, activation_not_needed (current window is still active; response includes `window_end`), not_applicable (offer is postpaid; renewal does not apply)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Forbidden — caller's pubkey does not match the offer's creator_pubkey",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Not found — offer_not_found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "409": {
            "description": "Conflict — offer_deactivated (offer is permanently deactivated and cannot be renewed)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error — activation_persist_failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "502": {
            "description": "Upstream failure — price_feed_unavailable, activation_creation_failed, fee_invoice_malformed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/offers/{id}": {
      "get": {
        "tags": ["Offers"],
        "summary": "Get offer details",
        "description": "Returns public details of an active offer. No authentication required. Sensitive fields (execution_webhook, metadata, signature) are not exposed. Returns 404 if the offer does not exist or is inactive.",
        "operationId": "getOffer",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "UUID of the offer"
          }
        ],
        "responses": {
          "200": {
            "description": "Offer details retrieved successfully",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GetOfferResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — missing offer id",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Not found — offer does not exist or is inactive",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only GET is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      },
      "delete": {
        "tags": ["Offers"],
        "summary": "Deactivate an offer",
        "description": "Deactivates an offer (sets status to 'deactivated'). Creator-only — pubkey must match the offer's creator_pubkey. Deactivated offers cannot receive new payments. Already deactivated offers return 409.",
        "operationId": "deactivateOffer",
        "security": [{ "PubkeySignature": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "UUID of the offer to deactivate"
          }
        ],
        "responses": {
          "200": {
            "description": "Offer deactivated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] },
                    "id": { "type": "string", "format": "uuid" },
                    "status": { "type": "string", "enum": ["deactivated"] }
                  }
                }
              }
            }
          },
          "403": { "description": "Forbidden — not the creator of this offer", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "404": { "description": "Not found — offer_not_found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "409": { "description": "Conflict — already_deactivated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/api/offers/{id}/pay": {
      "post": {
        "tags": ["Offers"],
        "summary": "Pay an offer (spawn payment intent)",
        "description": "Spawns a fresh payment intent for the specified offer and returns a Lightning invoice issued by the creator's receiving endpoint (Lightning Address or LNURL-pay). The payer pays the creator directly — Hypawave never receives the principal funds. After paying, the payer must call POST /api/offers/payment-intent/{id}/confirm with the preimage and payer_secret to prove settlement. No authentication required on this endpoint (optional payer_pubkey for attribution). Each call creates a new intent — offers can be paid multiple times. For fiat-priced offers, the amount is converted to sats at the current BTC price and locked at intent creation.\n\n**Activation gate (accountless offers only):** before spawning the invoice, the route verifies the offer has an active `offer_activations` row whose `window_end > now()` AND whose stored `terms_hash` still matches the current offer terms. If the activation is pending/expired or the window has elapsed → `402 offer_inactive`. If offer terms have been edited since activation → `409 terms_changed` (the activation row is left intact; creator can revert terms or call `/api/offers/{id}/renew`). All accountless offers are subject to this gate — there is no grandfathering bypass.\n\n**Balance gate (postpaid offers only):** verifies the creator's balance can cover the expected fee. Accountless offers skip this check — the fee is paid upfront via activation.\n\nOffer fields (description, webhook, amount, currency) are snapshotted onto the payment intent so it remains self-contained even if the offer changes later. Settlement status: pending → settled (fee deducted if postpaid, webhook fired) | expired | settled_unpaid_fee (postpaid safety net only; accountless never hits this state). Rate limited to 30 requests per minute per IP.",
        "operationId": "payOffer",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "UUID of the offer to pay"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PayOfferRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Payment intent created with fresh Lightning invoice",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PayOfferResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — missing offer id, invalid_offer_amount",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "402": {
            "description": "Payment required — offer_inactive (accountless offer: activation pending, expired, or missing; window has elapsed)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Not found — offer does not exist or is inactive",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "409": {
            "description": "Conflict — terms_changed (accountless offer's current terms no longer match the terms_hash snapshotted at activation; creator must revert terms or /renew)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "503": {
            "description": "Service unavailable — offer_temporarily_unavailable (postpaid only: creator balance cannot cover expected fee, or creator receiving endpoint is invalid/down/timed out; try again later)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/offers/payment-intent/{id}/confirm": {
      "post": {
        "tags": ["Offers"],
        "summary": "Confirm offer payment via preimage (direct-pay proof)",
        "description": "Proves that a direct payment to the creator settled, by submitting the Lightning preimage revealed by the payer's wallet. Hypawave verifies that SHA-256(preimage) matches the payment_hash stored on the intent, and verifies the payer_secret authorizes this payment intent. On success, the intent transitions pending → settled, the creator's fee is deducted, the execution webhook (if any) is fired, the offer's payment_count is incremented, and file access rows are created. Idempotent — repeated valid submissions of the same preimage return the same result and do not trigger double execution. This endpoint replaces the former webhook-driven settlement for persistent offers and is required to complete a Path 3b (non-custodial persistent offer) payment. Requirement: the payer must use programmable Lightning infrastructure (LND, CLN, Alby API, LNbits, NWC, etc.) that exposes the preimage after payment. Consumer wallets (Wallet of Satoshi, Phoenix, etc.) do not reliably expose the preimage.",
        "operationId": "confirmOfferPaymentIntent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "UUID of the payment intent being confirmed"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ConfirmPaymentIntentRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Payment intent confirmed (or already settled — idempotent)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ConfirmPaymentIntentResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — invalid_preimage (not 64-char hex), missing_payer_secret, preimage_mismatch (SHA-256 of preimage does not match stored payment_hash)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Unauthorized — payer_secret does not match stored payer_secret_hash",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Not found — payment_intent_not_found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/offers/payment-intent/{id}/status": {
      "get": {
        "tags": ["Offers"],
        "summary": "Check payment intent status",
        "description": "Returns the current status of a payment intent. Authenticated via payer_secret query parameter (SHA256(secret) must match stored payer_secret_hash). If the intent is settled and has files, a time-limited claim_token is generated for retrieving file keys via the /file-key endpoint.",
        "operationId": "getPaymentIntentStatus",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "UUID of the payment intent" },
          { "name": "secret", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Payer secret (hex) returned by /api/offers/{id}/pay" }
        ],
        "responses": {
          "200": {
            "description": "Payment intent status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payment_intent_id": { "type": "string", "format": "uuid" },
                    "offer_id": { "type": "string", "format": "uuid" },
                    "status": { "type": "string", "enum": ["pending", "settled", "expired"] },
                    "settled_at": { "type": "string", "format": "date-time", "nullable": true },
                    "claim_token": { "type": "string", "description": "Time-limited token for retrieving file keys (only present when settled and files exist)" },
                    "token_expires_at": { "type": "string", "format": "date-time", "description": "Expiration of the claim_token (72 hours)" }
                  }
                }
              }
            }
          },
          "400": { "description": "Bad request — missing id or secret", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "401": { "description": "Unauthorized — invalid payer_secret", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/api/offers/payment-intent/{id}/receipt": {
      "get": {
        "tags": ["Offers"],
        "summary": "Get payment intent receipt",
        "description": "Returns a receipt for a settled payment intent with cryptographic proof of payment. Authenticated via payer_secret query parameter. Only available after the intent reaches 'settled' status.",
        "operationId": "getPaymentIntentReceipt",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "UUID of the payment intent" },
          { "name": "secret", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Payer secret (hex) returned by /api/offers/{id}/pay" }
        ],
        "responses": {
          "200": {
            "description": "Payment receipt",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "receipt": {
                      "type": "object",
                      "properties": {
                        "payment_intent_id": { "type": "string", "format": "uuid" },
                        "offer_id": { "type": "string", "format": "uuid" },
                        "status": { "type": "string" },
                        "payment_hash": { "type": "string", "nullable": true },
                        "preimage": { "type": "string", "nullable": true },
                        "receipt_verified": { "type": "boolean", "description": "true if SHA256(preimage) === payment_hash" },
                        "locked_amount_sats": { "type": "integer", "nullable": true },
                        "locked_fiat_amount": { "type": "number", "nullable": true },
                        "locked_currency": { "type": "string", "nullable": true },
                        "locked_btc_rate": { "type": "number", "nullable": true },
                        "offer_description": { "type": "string", "nullable": true },
                        "offer_amount": { "type": "number", "nullable": true },
                        "offer_currency": { "type": "string", "nullable": true },
                        "created_at": { "type": "string", "format": "date-time" },
                        "settled_at": { "type": "string", "format": "date-time", "nullable": true },
                        "has_files": { "type": "boolean" },
                        "file_count": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Bad request — missing id or secret, or intent not settled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "401": { "description": "Unauthorized — invalid payer_secret", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/api/offers/payment-intent/{id}/file-key": {
      "get": {
        "tags": ["Offers", "Files"],
        "summary": "Retrieve offer file decryption keys",
        "description": "Returns decryption keys and file metadata for all files attached to a settled payment intent. Authenticated via claim_token (from the /status endpoint) or preimage (from the payer's Lightning payment). The claim_token expires after 72 hours. Download claims are tracked.",
        "operationId": "getOfferFileKeys",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "UUID of the payment intent" },
          { "name": "claim_token", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Claim token from /status response (alternative to preimage)" },
          { "name": "preimage", "in": "query", "required": false, "schema": { "type": "string" }, "description": "64-char hex Lightning preimage (alternative to claim_token)" }
        ],
        "responses": {
          "200": {
            "description": "File keys and metadata",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "files": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "offer_file_id": { "type": "string", "format": "uuid" },
                          "filename": { "type": "string" },
                          "content_type": { "type": "string", "nullable": true },
                          "size": { "type": "integer" },
                          "storage_key": { "type": "string", "description": "R2 object key for the encrypted file" },
                          "iv_hex": { "type": "string", "description": "Hex IV for decryption" },
                          "wrapped_key": { "type": "string", "description": "Encrypted file key" },
                          "key_version": { "type": "integer", "nullable": true },
                          "wrap_algo": { "type": "string", "nullable": true }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Bad request — provide claim_token or preimage", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "401": { "description": "Unauthorized — invalid or expired token/preimage", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/api/offers/payment-intent/{id}/download-url": {
      "post": {
        "tags": ["Offers", "Files"],
        "summary": "Generate signed download URL for offer file",
        "description": "Generates a time-limited signed URL (5 minutes) to download a single encrypted offer file from R2 storage. Scoped to a specific file — the offer_file_id must belong to the offer associated with the payment intent and the payer must have been granted access. Authenticated via claim_token or preimage (same as /file-key). Use the returned URL to fetch the encrypted file, then decrypt with the key from /file-key.",
        "operationId": "generateOfferDownloadUrl",
        "x-rateLimit": {
          "ip": "30 requests per minute per IP"
        },
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "UUID of the payment intent" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["offer_file_id"],
                "properties": {
                  "offer_file_id": { "type": "string", "format": "uuid", "description": "UUID of the offer file to download (from /file-key response)" },
                  "claim_token": { "type": "string", "description": "Claim token from /status response (alternative to preimage)" },
                  "preimage": { "type": "string", "description": "64-char hex Lightning preimage (alternative to claim_token)" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed download URL generated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "downloadUrl": { "type": "string", "format": "uri", "description": "Time-limited signed URL to download the encrypted file (expires in 5 minutes)" }
                  }
                }
              }
            }
          },
          "400": { "description": "Bad request — missing offer_file_id or auth credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "401": { "description": "Unauthorized — invalid or expired token/preimage", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Forbidden — file_not_associated or access_not_granted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "404": { "description": "Not found — payment_intent_not_found or no_file_stored", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/api/agent/create-invoice": {
      "post": {
        "tags": ["Invoices"],
        "summary": "Create a payment request",
        "description": "Creates a new payment request. The fiat amount is converted to satoshis at the current BTC price and locked at creation time. Returns a payment URL for settlement. Payments go directly to the API key owner's stored lightning_address (auto-injected server-side — agents never supply or manage the payment destination). Once the Lightning payment confirms, any attached encrypted data is deterministically unlocked — no further authorization required. State machine: created → settled → unlocked | expired. Rate limited to 30 requests per minute per identity.",
        "operationId": "createInvoice",
        "security": [{ "BearerAuth": [] }],
        "x-rateLimit": {
          "agent": "30 requests per minute per identity",
          "daily": "Configurable daily invoice limit per user"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateInvoiceRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Payment request created successfully",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreateInvoiceResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — validation_error (with message and field), identity_not_found, invalid_currency, amount_below_minimum (includes min_usd), amount_above_maximum (includes max_usd), invalid_payment_destination, invalid_payment_destination_domain, user_not_found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed — missing_auth, invalid_key_format, invalid_api_key, api_key_revoked",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthErrorResponse" }
              }
            }
          },
          "402": {
            "description": "Payment required — negative_balance (outstanding settlement fees block new requests, never delivery)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — rate_limit_exceeded or daily_limit",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error — internal_error, stream_creation_failed, invoice_creation_failed, transaction_insert_failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "502": {
            "description": "Bad gateway — price_feed_unavailable (BTC price feed unreachable)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "503": {
            "description": "Service unavailable — circuit_breaker (system temporarily unavailable)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/paystream-cb": {
      "get": {
        "tags": ["Invoices", "Offers"],
        "summary": "Fetch creator-issued Lightning bolt11 for payment",
        "description": "Resolves the creator's Lightning Address or LNURL-pay endpoint and returns a fresh bolt11 invoice for the payer to pay directly. Records an invoice_payment_attempts row binding the bolt11's payment_hash to the invoice. This is the payer's entry point for paying any invoice (Paths 1, 2, 3a). No authentication required — the access_token in the query string identifies the invoice. For accountless invoices (Path 3a), the creator's activation fee must have settled first — otherwise returns 402 activation_pending.",
        "operationId": "getPaymentBolt11",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "description": "The access_token returned by create-invoice",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Creator-issued bolt11 returned successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pr": { "type": "string", "description": "Lightning bolt11 invoice — pay this directly to the creator" },
                    "routes": { "type": "array", "items": {}, "description": "Routing hints (typically empty)" },
                    "terms_hash": { "type": "string", "description": "SHA-256 hash of the invoice's canonical terms (amount, currency, description, payment_destination, btc_amount). Submit this back to POST /api/invoice/{id}/confirm to verify terms have not changed between bolt11 fetch and settlement confirmation." }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — missing or invalid token, invoice not found, creator Lightning Address missing or invalid",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "402": {
            "description": "Payment required — activation_pending (Path 3a: creator's activation fee has not settled yet)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "502": {
            "description": "Bad gateway — creator's Lightning endpoint unreachable, creator_invoice_reused, or creator_invoice_malformed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/invoice/{id}/confirm": {
      "post": {
        "tags": ["Invoices"],
        "summary": "Confirm invoice payment with preimage",
        "description": "Payer-facing settlement confirmation for Paths 2 and 3a. After paying the creator-issued Lightning bolt11, submit the payment_hash and preimage to confirm settlement and trigger key release. No API key or webhook secret required — authorization is cryptographic: invoice_id (URL) + payment_hash + SHA256(preimage) == payment_hash + matching unconsumed invoice_payment_attempts row. Requires programmable Lightning infrastructure (LND, CLN, Alby API, LNbits, NWC, etc.) that exposes the preimage after payment. Consumer wallets (Wallet of Satoshi, Phoenix, etc.) do not reliably expose the preimage. Idempotent — safe to call multiple times. Rate limited to 30 requests per minute per IP.",
        "operationId": "confirmInvoicePayment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice_id returned by create-invoice",
            "schema": { "type": "integer" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["payment_hash", "preimage"],
                "properties": {
                  "payment_hash": {
                    "type": "string",
                    "pattern": "^[0-9a-fA-F]{64}$",
                    "description": "Hex-encoded 32-byte payment hash (64 hex chars) from the Lightning payment"
                  },
                  "preimage": {
                    "type": "string",
                    "pattern": "^[0-9a-fA-F]{64}$",
                    "description": "Hex-encoded 32-byte preimage (64 hex chars) revealed by your Lightning node on successful payment. SHA256(preimage) must equal payment_hash"
                  },
                  "terms_hash": {
                    "type": "string",
                    "description": "Optional. SHA-256 terms hash from the paystream-cb response. If provided, the server recomputes the hash from current invoice terms and rejects with 409 terms_changed on mismatch. Recommended for pre-authorization — ensures terms have not changed between bolt11 fetch and settlement confirmation."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Settlement confirmed (or already settled on retry)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "example": true },
                    "already_settled": { "type": "boolean", "description": "true if this invoice was already settled by a prior call" }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — invalid_invoice_id, missing_proof, invalid_payment_hash, invalid_preimage, preimage_hash_mismatch, invoice_missing_destination, no_matching_attempt",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Not found — invoice_not_found, transaction_not_found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "409": {
            "description": "Conflict — payment_hash_replay (preimage already used for a different transaction), terms_changed (invoice terms modified since bolt11 was fetched — refetch via paystream-cb)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/invoice/{id}/receipt": {
      "get": {
        "tags": ["Invoices"],
        "summary": "Get payer receipt for a settled invoice",
        "description": "Returns a receipt for a settled invoice. Authenticated via preimage query parameter — the payer proves they paid by providing the preimage (SHA256(preimage) must match the stored payment_hash). No API key or account required. Available after settlement for Paths 1, 2, and 3a invoices.",
        "operationId": "getPayerInvoiceReceipt",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Invoice ID" },
          { "name": "preimage", "in": "query", "required": true, "schema": { "type": "string", "pattern": "^[0-9a-fA-F]{64}$" }, "description": "64-char hex preimage from the Lightning payment — proves the caller paid this invoice" }
        ],
        "responses": {
          "200": {
            "description": "Payer receipt with settlement proof",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "receipt": {
                      "type": "object",
                      "properties": {
                        "invoice_id": { "type": "integer" },
                        "status": { "type": "string", "enum": ["settled"] },
                        "amount": { "type": "number" },
                        "currency": { "type": "string" },
                        "amount_sats": { "type": "integer", "nullable": true },
                        "description": { "type": "string", "nullable": true },
                        "creator": {
                          "type": "object",
                          "properties": {
                            "first_name": { "type": "string", "nullable": true },
                            "last_name": { "type": "string", "nullable": true },
                            "company": { "type": "string", "nullable": true }
                          }
                        },
                        "created_at": { "type": "string", "format": "date-time" },
                        "settled_at": { "type": "string", "format": "date-time", "nullable": true },
                        "expires_at": { "type": "string", "format": "date-time", "nullable": true },
                        "payment_hash": { "type": "string" },
                        "preimage": { "type": "string", "nullable": true },
                        "receipt_verified": { "type": "boolean" },
                        "has_files": { "type": "boolean" },
                        "file_count": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Bad request — invalid_invoice_id, invalid_preimage", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "401": { "description": "Unauthorized — preimage does not match a settled payment", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "404": { "description": "Not found — invoice_not_found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/api/agent/list-invoices": {
      "get": {
        "tags": ["Invoices"],
        "summary": "List payment requests",
        "description": "Returns a paginated list of payment requests created by the authenticated identity. Includes settlement status, file attachment info, and payment proofs. Rate limited to 30 requests per minute per identity.",
        "operationId": "listInvoices",
        "security": [{ "BearerAuth": [] }],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "default": 10, "minimum": 1, "maximum": 50 },
            "description": "Number of results per page (default 10, max 50)"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "default": 0, "minimum": 0 },
            "description": "Number of results to skip for pagination"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["paid", "not_paid", "expired"] },
            "description": "Filter by payment status"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of payment requests",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ListInvoicesResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/agent/upload-url": {
      "post": {
        "tags": ["Files"],
        "summary": "Get presigned upload URL",
        "description": "Generates a presigned URL for uploading an encrypted file directly to object storage. The URL expires after 1 hour. After uploading, use store-file to record the metadata and store-file-key to store the encryption key. Rate limited to 30 requests per minute per identity.",
        "operationId": "getUploadUrl",
        "security": [{ "BearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/UploadUrlRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Presigned upload URL generated",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/UploadUrlResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — missing fields, invalid content type, or file too large",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/agent/store-file": {
      "post": {
        "tags": ["Files"],
        "summary": "Store encrypted file metadata",
        "description": "Records metadata for an encrypted file uploaded via the presigned URL. The file must be attached to an invoice owned by the authenticated identity. Returns the file record ID needed for store-file-key. Rate limited to 30 requests per minute per identity.",
        "operationId": "storeFile",
        "security": [{ "BearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/StoreFileRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "File metadata stored successfully",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/StoreFileResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — missing required fields",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Forbidden — invoice belongs to another identity",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Invoice not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/agent/store-file-key": {
      "post": {
        "tags": ["Files"],
        "summary": "Store encryption key for file",
        "description": "Stores the AES-256-GCM encryption key for a previously stored file record. The key is held in escrow and released deterministically upon settlement confirmation. Only the invoice owner can store keys. Rate limited to 30 requests per minute per identity.",
        "operationId": "storeFileKey",
        "security": [{ "BearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/StoreFileKeyRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Encryption key stored successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok"],
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — missing required fields",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Forbidden — invoice belongs to another identity",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "File or invoice not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/agent/balance": {
      "get": {
        "tags": ["Balance"],
        "summary": "Get settlement balance",
        "description": "Returns the current balance in satoshis and fiat equivalent. A negative balance means settlement fees are outstanding — this blocks new payment request creation but never blocks delivery or key release on already-settled requests. Rate limited to 30 requests per minute per identity.",
        "operationId": "getBalance",
        "security": [{ "BearerAuth": [] }],
        "x-rateLimit": {
          "agent": "30 requests per minute per identity"
        },
        "parameters": [
          {
            "name": "currency",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "USD"
            },
            "description": "ISO 4217 currency code for fiat conversion (default: USD)"
          }
        ],
        "responses": {
          "200": {
            "description": "Balance retrieved successfully",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BalanceResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed — missing_auth, invalid_key_format, invalid_api_key, api_key_revoked",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only GET is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/agent/topup": {
      "post": {
        "tags": ["Balance"],
        "summary": "Settle outstanding fees",
        "description": "Creates a Lightning invoice for exactly the amount the agent owes in outstanding settlement fees (= -balance.available_sats when negative). Top-ups are fee-settlement only; they never pre-fund. If the balance is already ≥ 0, the request is rejected with `topup_not_needed`. Request body is ignored. Rate limited to 30 requests per minute per identity. Settlement is handled by the LNbits webhook (with polling fallback) via `credit_topup`, which caps crediting so the balance can never exceed 0.",
        "operationId": "topupBalance",
        "security": [{ "BearerAuth": [] }],
        "x-rateLimit": {
          "agent": "30 requests per minute per identity"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/TopupRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lightning invoice for the exact owed amount — pay it to settle outstanding fees",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TopupResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — topup_not_needed (balance is already ≥ 0; includes available_sats)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed — missing_auth, invalid_key_format, invalid_api_key, api_key_revoked",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "409": {
            "description": "Conflict — duplicate_topup (this top-up is already pending)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error — lnbits_not_configured, topup_insert_failed, internal_error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "502": {
            "description": "Bad gateway — price_feed_unavailable, lnbits_no_invoice, could_not_decode_payment_hash",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/agent/create-key": {
      "post": {
        "tags": ["Keys"],
        "summary": "Create a new API key",
        "description": "Creates a new agent API key. Requires session/cookie authentication (logged-in user), not Bearer token auth. The full key is returned only once — store it securely. Rate limited to 5 per hour per IP and 3 per hour per identity.",
        "operationId": "createKey",
        "x-rateLimit": {
          "ip": "5 per hour per IP",
          "identity": "3 per hour per identity"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateKeyRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "API key created successfully",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreateKeyResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — identity_not_found (register identity first)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — too many key creation requests",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error — key_creation_failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/agent/revoke-key": {
      "post": {
        "tags": ["Keys"],
        "summary": "Revoke an API key",
        "description": "Revokes an existing API key. Requires session/cookie authentication (logged-in user), not Bearer token auth. Only the key owner can revoke their keys.",
        "operationId": "revokeKey",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RevokeKeyRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Key revoked successfully or was already revoked",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RevokeKeyResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — missing_key_id, identity_not_found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Forbidden — not_your_key (key belongs to another identity)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Not found — key_not_found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error — revoke_failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/agent/list-keys": {
      "get": {
        "tags": ["Keys"],
        "summary": "List all API keys",
        "description": "Returns all API keys for the authenticated user's identity. Requires session/cookie authentication (logged-in user), not Bearer token auth. Keys are returned in reverse chronological order.",
        "operationId": "listKeys",
        "responses": {
          "200": {
            "description": "List of API keys",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ListKeysResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only GET is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error — fetch_failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/agent/receipt": {
      "get": {
        "tags": ["Invoices"],
        "summary": "Get invoice receipt",
        "description": "Returns a receipt for a settled invoice with cryptographic proof of payment. Only available for invoices created by the authenticated identity. Supports Bearer token (Path 2 agents) and secp256k1 pubkey signature (Path 3a accountless agents). The receipt includes creator details, payment proof (payment_hash, preimage), and receipt_verified (true if SHA256(preimage) === payment_hash). Returns 400 if invoice is not yet settled.",
        "operationId": "getInvoiceReceipt",
        "security": [{ "BearerAuth": [] }, { "PubkeySignature": [] }],
        "parameters": [
          { "name": "invoice_id", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "Invoice ID to fetch receipt for" }
        ],
        "responses": {
          "200": {
            "description": "Invoice receipt with settlement proof",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "receipt": {
                      "type": "object",
                      "properties": {
                        "invoice_id": { "type": "integer" },
                        "status": { "type": "string", "enum": ["settled"] },
                        "amount": { "type": "number" },
                        "currency": { "type": "string" },
                        "amount_sats": { "type": "integer", "nullable": true },
                        "description": { "type": "string", "nullable": true },
                        "creator": {
                          "type": "object",
                          "properties": {
                            "first_name": { "type": "string", "nullable": true },
                            "last_name": { "type": "string", "nullable": true },
                            "company": { "type": "string", "nullable": true }
                          }
                        },
                        "created_at": { "type": "string", "format": "date-time" },
                        "settled_at": { "type": "string", "format": "date-time", "nullable": true },
                        "expires_at": { "type": "string", "format": "date-time", "nullable": true },
                        "payment_hash": { "type": "string", "nullable": true },
                        "preimage": { "type": "string", "nullable": true },
                        "receipt_verified": { "type": "boolean", "description": "true if SHA256(preimage) === payment_hash" },
                        "has_files": { "type": "boolean" },
                        "file_count": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Bad request — missing_invoice_id, invalid_invoice_id, or invoice_not_settled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "404": { "description": "Not found — invoice_not_found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/api/get-unlock-status": {
      "post": {
        "tags": ["Settlement"],
        "summary": "Verify settlement status",
        "description": "Checks whether payment requests have reached settlement and triggered deterministic unlock. Returns per-request status including stream state and failure reasons. Settlement confirmation is unconditional — once payment confirms, unlock is guaranteed regardless of creator balance or platform state. Supports triple authentication: Bearer token (agent), session cookie (user), or secp256k1 pubkey signature (accountless agent). Pubkey-authenticated callers can only poll invoices where creator_identity_id matches their identity. Rate limited to 30 per minute per IP.",
        "operationId": "getUnlockStatus",
        "security": [{ "BearerAuth": [] }, { "PubkeySignature": [] }],
        "x-rateLimit": {
          "ip": "30 requests per minute per IP"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/UnlockStatusRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Settlement and unlock status for requested payment requests",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/UnlockStatusResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — missing or invalid invoice_ids",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Unauthorized — authentication required (Bearer or session)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/get-key": {
      "get": {
        "tags": ["Settlement"],
        "summary": "Retrieve encryption key after settlement",
        "description": "Retrieves the AES-256-GCM encryption key and IV for a settled payment request's attached file. Requires authentication via access_token (query parameter), Bearer token (agent), or session cookie (user). The invoice must be in 'paid' status. The key can only be retrieved within the download window (72 hours from first claim). Rate limited to 30 per minute per IP.",
        "operationId": "getFileKey",
        "security": [{ "BearerAuth": [] }],
        "x-rateLimit": {
          "ip": "30 requests per minute per IP"
        },
        "parameters": [
          {
            "name": "invoice_file_id",
            "in": "query",
            "required": true,
            "schema": { "type": "string" },
            "description": "UUID of the attached file to retrieve the encryption key for (after settlement)"
          },
          {
            "name": "token",
            "in": "query",
            "required": false,
            "schema": { "type": "string" },
            "description": "Access token for the invoice (alternative to Bearer/session auth)"
          }
        ],
        "responses": {
          "200": {
            "description": "Encryption key and IV returned successfully",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GetKeyResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — missing invoice_file_id parameter",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Forbidden — not_paid (payment request not yet settled), key_already_released (key was already claimed)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Not found — file_not_found, invoice_not_found, key_not_found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "410": {
            "description": "Gone — invoice_expired (payment request has passed its expiration plus grace period)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error — atomic_update_failed, server_error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/get-invoice-files": {
      "post": {
        "tags": ["Files", "Invoices"],
        "summary": "Get file metadata for invoices",
        "description": "Returns file metadata (IDs, names, encrypted URLs) for up to 25 invoices. Supports Bearer token (agent), session cookie (user), or access_token authentication. Only returns files for invoices owned by or authorized for the authenticated identity.",
        "operationId": "getInvoiceFiles",
        "security": [{ "BearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["invoice_ids"],
                "properties": {
                  "invoice_ids": { "type": "array", "items": { "type": "integer" }, "maxItems": 25, "description": "Array of invoice IDs to fetch files for" },
                  "token": { "type": "string", "description": "Optional access_token for client authorization (alternative to Bearer/session auth)" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "File metadata grouped by invoice ID",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "files": {
                      "type": "object",
                      "description": "Map of invoice_id to array of file records",
                      "additionalProperties": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "id": { "type": "string", "description": "File UUID — use with /api/generate-download-url and /api/get-key" },
                            "encrypted_file_url": { "type": "string", "description": "R2 storage key for the encrypted file" },
                            "file_name": { "type": "string", "description": "Original file name" }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Bad request — missing or invalid invoice_ids", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/api/generate-download-url": {
      "post": {
        "tags": ["Files"],
        "summary": "Generate signed download URL for encrypted file",
        "description": "Generates a time-limited signed URL (5 minutes) to download an encrypted file from R2 storage. The invoice must be paid. Supports Bearer token (agent), session cookie (user), or access_token authentication. Use the returned URL to fetch the encrypted file, then decrypt with the key from /api/get-key.",
        "operationId": "generateDownloadUrl",
        "security": [{ "BearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["invoice_file_id"],
                "properties": {
                  "invoice_file_id": { "type": "string", "description": "UUID of the file (from /api/get-invoice-files response)" },
                  "token": { "type": "string", "description": "Optional access_token for client authorization (alternative to Bearer/pubkey auth)" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed download URL generated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "downloadUrl": { "type": "string", "format": "uri", "description": "Time-limited signed URL to download the encrypted file (expires in 5 minutes)" }
                  }
                }
              }
            }
          },
          "400": { "description": "Bad request — missing invoice_file_id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Forbidden — not_paid or unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "404": { "description": "Not found — file_not_found or invoice_not_found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/api/public-settings": {
      "get": {
        "tags": ["Settings"],
        "summary": "Get current platform configuration",
        "description": "Returns current admin-configurable platform settings including fee percentage, invoice amount limits, file limits, and BTC price. No authentication required. Cached for 30 seconds. Use these values to validate amounts and understand current fee policy before creating invoices or offers.",
        "operationId": "getPublicSettings",
        "responses": {
          "200": {
            "description": "Current platform settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "fee_percent": { "type": "number", "description": "Current service fee percentage (e.g. 1 = 1%)" },
                    "min_invoice_usd": { "type": "number", "description": "Minimum invoice amount in USD equivalent" },
                    "max_invoice_usd": { "type": "number", "description": "Maximum invoice amount in USD equivalent" },
                    "max_file_size_mb": { "type": "number", "description": "Maximum file size in MB" },
                    "max_files_per_invoice": { "type": "integer", "description": "Maximum files per invoice" },
                    "min_topup_sats": { "type": "integer", "description": "Minimum topup amount in satoshis" },
                    "min_topup_usd": { "type": "number", "nullable": true, "description": "Minimum topup in USD (null if BTC price unavailable)" },
                    "btc_usd_price": { "type": "number", "nullable": true, "description": "Current BTC/USD price (null if feed unavailable)" }
                  }
                }
              }
            }
          },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/api/register-identity": {
      "post": {
        "tags": ["Identity"],
        "summary": "Register or retrieve user identity",
        "description": "Registers a new identity for the authenticated user by associating a secp256k1 compressed public key, or returns the existing active identity if one already exists. Requires session cookie authentication with CSRF token. This is the bootstrap endpoint for accountless onboarding — call it once after sign-in to obtain your identity_id. Rate limited to 10 requests per minute per IP.",
        "operationId": "registerIdentity",
        "security": [],
        "x-rateLimit": {
          "ip": "10 requests per minute per IP"
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pubkey": {
                    "type": "string",
                    "pattern": "^[0-9a-fA-F]{66}$",
                    "description": "Compressed secp256k1 public key (66-char hex). Required only when creating a new identity; ignored if an active identity already exists for the session user."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing active identity returned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "identity": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string", "format": "uuid" },
                        "pubkey": { "type": "string" },
                        "type": { "type": "string" },
                        "status": { "type": "string" }
                      }
                    },
                    "created": { "type": "boolean", "enum": [false] }
                  }
                }
              }
            }
          },
          "201": {
            "description": "New identity created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "identity": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string", "format": "uuid" },
                        "pubkey": { "type": "string" },
                        "type": { "type": "string" },
                        "status": { "type": "string" }
                      }
                    },
                    "created": { "type": "boolean", "enum": [true] }
                  }
                }
              }
            }
          },
          "400": { "description": "Bad request — missing_pubkey or invalid_pubkey_format", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "401": { "description": "Unauthorized — no valid session", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/api/offers/list": {
      "get": {
        "tags": ["Offers"],
        "summary": "List own offers",
        "description": "Returns a list (up to 100) of offers created by the authenticated pubkey. Filterable by status. Defaults to `active` offers when no status filter is provided. Rate limited to 30 requests per minute per IP.",
        "operationId": "listOffers",
        "security": [{ "PubkeySignature": [] }],
        "parameters": [
          {
            "name": "x-pubkey",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 public key"
          },
          {
            "name": "x-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 signature over the signed payload hash"
          },
          {
            "name": "x-signed-payload-hash",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded SHA-256 hash of the canonical empty body"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["active", "deactivated", "exhausted"], "default": "active" },
            "description": "Filter offers by status (default: active)"
          }
        ],
        "responses": {
          "200": {
            "description": "List of offers",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ListOffersResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only GET is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/offers/create-invoice": {
      "post": {
        "tags": ["Offers"],
        "summary": "Create a payment request (Offers API, Path 3a)",
        "description": "Creates a new payment request under pubkey-signature authentication. Identical semantics to the Agent API create-invoice but authenticated via secp256k1 keypair instead of Bearer token. Path 3a is non-custodial: creation mints an upfront activation fee bolt11 (fee computed from declared amount — see GET /api/public-settings for fee_percent) via LNbits Cloud. The response includes a top-level `activation` sibling containing `fee_bolt11` — the creator must pay this using programmable Lightning infrastructure (LND, CLN, Alby API, LNbits, etc.) before the invoice becomes payable. Until then, payer-facing routes respond with `402 activation_pending`. The activation fee is earned at creation, not at payer settlement. No refunds. Accountless identities do not write to balances (PR2 2026-04-17); no outstanding-fee check applies. State machine: created (pending activation) → active → settled → unlocked | expired. Rate limited.",
        "operationId": "createOfferInvoice",
        "security": [{ "PubkeySignature": [] }],
        "parameters": [
          {
            "name": "x-pubkey",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 public key"
          },
          {
            "name": "x-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 signature over the signed payload hash"
          },
          {
            "name": "x-signed-payload-hash",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded SHA-256 hash of the request body"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateOfferInvoiceRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Payment request created successfully. Response includes a top-level `activation` object — the creator must pay `activation.fee_bolt11` before the invoice becomes payable.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreateOfferInvoiceResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — validation_error, invalid_currency, amount_below_minimum, amount_above_maximum, invalid_payment_destination",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed — missing or invalid pubkey signature headers",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — daily_limit or per-minute throttle",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error — activation_fee_calc_failed (unreachable in practice), activation_persist_failed (fee bolt11 minted but DB insert failed — invoice is inert; see logs for payment_hash), internal_error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "502": {
            "description": "Bad gateway — price_feed_unavailable, activation_creation_failed (LNbits mint failed), fee_invoice_malformed (minted bolt11 without decodable payment_hash or expires_at)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "503": {
            "description": "Service unavailable — circuit_breaker",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/offers/list-invoices": {
      "get": {
        "tags": ["Offers"],
        "summary": "List payment requests (Offers API)",
        "description": "Returns a paginated list of payment requests created by the authenticated pubkey identity. Includes settlement status, payer details, file attachment info, and payment proofs. Filterable by status (paid, not_paid, expired).",
        "operationId": "listOfferInvoices",
        "security": [{ "PubkeySignature": [] }],
        "parameters": [
          {
            "name": "x-pubkey",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 public key"
          },
          {
            "name": "x-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 signature over the signed payload hash"
          },
          {
            "name": "x-signed-payload-hash",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded SHA-256 hash of the canonical empty body"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "default": 20, "minimum": 1, "maximum": 50 },
            "description": "Number of results per page (default 20, max 50)"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "default": 0, "minimum": 0 },
            "description": "Number of results to skip for pagination"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["paid", "not_paid", "expired"] },
            "description": "Filter by payment status"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of payment requests",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/OffersListInvoicesResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only GET is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/offers/list-payments": {
      "get": {
        "tags": ["Offers"],
        "summary": "List payment intents for own offers",
        "description": "Returns a paginated list of payment intents spawned from offers created by the authenticated pubkey identity. Filterable by status and offer_id. Includes settlement proofs, payer pubkeys, and webhook execution status.",
        "operationId": "listOfferPayments",
        "security": [{ "PubkeySignature": [] }],
        "parameters": [
          {
            "name": "x-pubkey",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 public key"
          },
          {
            "name": "x-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 signature over the signed payload hash"
          },
          {
            "name": "x-signed-payload-hash",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded SHA-256 hash of the canonical empty body"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "default": 20, "minimum": 1, "maximum": 50 },
            "description": "Number of results per page (default 20, max 50)"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "default": 0, "minimum": 0 },
            "description": "Number of results to skip for pagination"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["pending", "settled", "expired", "settled_unpaid_fee"] },
            "description": "Filter by payment intent status"
          },
          {
            "name": "offer_id",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "format": "uuid" },
            "description": "Filter by a specific offer UUID"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of payment intents",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ListPaymentsResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only GET is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/offers/upload-url": {
      "post": {
        "tags": ["Files"],
        "summary": "Get presigned upload URL (Offers API)",
        "description": "Generates a presigned URL for uploading an encrypted file directly to object storage, authenticated via pubkey signature. The URL expires after 1 hour. After uploading, use store-file (for offer files) or store-invoice-file (for invoice files) to record the metadata.",
        "operationId": "getOffersUploadUrl",
        "security": [{ "PubkeySignature": [] }],
        "parameters": [
          {
            "name": "x-pubkey",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 public key"
          },
          {
            "name": "x-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 signature over the signed payload hash"
          },
          {
            "name": "x-signed-payload-hash",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded SHA-256 hash of the request body"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/UploadUrlRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Presigned upload URL generated",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/UploadUrlResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — missing fields, invalid_content_type, or file_too_large",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/offers/store-file": {
      "post": {
        "tags": ["Files"],
        "summary": "Store encrypted offer file metadata",
        "description": "Records metadata for an encrypted file attached to an offer. The offer must be owned by the authenticated pubkey. Returns the `offer_file_id` needed to store the encryption key via store-file-key. File count is limited per offer by server policy.",
        "operationId": "storeOfferFile",
        "security": [{ "PubkeySignature": [] }],
        "parameters": [
          {
            "name": "x-pubkey",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 public key"
          },
          {
            "name": "x-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 signature over the signed payload hash"
          },
          {
            "name": "x-signed-payload-hash",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded SHA-256 hash of the request body"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/StoreOfferFileRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Offer file metadata stored successfully",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/StoreOfferFileResponse" }
              }
            }
          },
          "400": {
            "description": "Bad request — missing required fields or file count limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Forbidden — offer belongs to another pubkey",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Offer not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/offers/store-file-key": {
      "post": {
        "tags": ["Files"],
        "summary": "Store encryption key for offer file",
        "description": "Stores the wrapped AES-256-GCM encryption key for a previously stored offer file. The key is held in escrow and released upon settlement of a corresponding payment intent. Only the offer owner can store keys. Upserts on conflict — safe to call again if the first attempt was ambiguous.",
        "operationId": "storeOfferFileKey",
        "security": [{ "PubkeySignature": [] }],
        "parameters": [
          {
            "name": "x-pubkey",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 public key"
          },
          {
            "name": "x-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 signature over the signed payload hash"
          },
          {
            "name": "x-signed-payload-hash",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded SHA-256 hash of the request body"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/StoreOfferFileKeyRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Encryption key stored successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok"],
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — missing required fields",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Forbidden — offer belongs to another pubkey",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Offer file or offer not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/offers/store-invoice-file": {
      "post": {
        "tags": ["Files"],
        "summary": "Attach encrypted file to invoice (Offers API)",
        "description": "Records metadata for an encrypted file attached to a payment request (invoice), authenticated via pubkey signature. The invoice must be owned by the authenticated identity. File count is limited per invoice by server policy. Rate limited to 20 requests per minute per IP.",
        "operationId": "storeOfferInvoiceFile",
        "security": [{ "PubkeySignature": [] }],
        "parameters": [
          {
            "name": "x-pubkey",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 public key"
          },
          {
            "name": "x-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 signature over the signed payload hash"
          },
          {
            "name": "x-signed-payload-hash",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded SHA-256 hash of the request body"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/StoreOfferInvoiceFileRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invoice file metadata stored successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "id"],
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] },
                    "id": { "type": "string", "description": "UUID of the stored invoice file record — use as invoice_file_id in invoice-file-key" }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — missing required fields or file count limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Forbidden — invoice belongs to another identity",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Invoice not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/offers/invoice-file-key": {
      "post": {
        "tags": ["Files"],
        "summary": "Store encryption key for invoice file (Offers API)",
        "description": "Stores the AES-256-GCM encryption key for an invoice file record, authenticated via pubkey signature. The key is held in escrow and released deterministically upon settlement confirmation. Only the invoice owner can store keys. Rate limited to 20 requests per minute per IP.",
        "operationId": "storeOfferInvoiceFileKey",
        "security": [{ "PubkeySignature": [] }],
        "parameters": [
          {
            "name": "x-pubkey",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 public key"
          },
          {
            "name": "x-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded secp256k1 signature over the signed payload hash"
          },
          {
            "name": "x-signed-payload-hash",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Hex-encoded SHA-256 hash of the request body"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/InvoiceFileKeyRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invoice file encryption key stored successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok"],
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request — missing params (invoice_file_id and key_b64 required)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Forbidden — invoice belongs to another identity",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Invoice file or invoice not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST is accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    }
  }
}
