{
  "openapi": "3.1.0",
  "info": {
    "title": "LanaPay POS API",
    "version": "1.0.0",
    "summary": "Server-to-server payment API for physical tills: sales settled in LANA or cash.",
    "description": "LanaPay POS lets a physical till accept LANA and cash. The POS creates a **sale** priced in fiat,\nscans the customer's Lana card, and sends one pay call with the scan as an **opaque value** — the\nanswer is synchronous and final: paid with exact amounts, or a typed refusal the cashier can act on.\nThe scan is interpreted server-side, so POS/ERP integrations never change when new currencies arrive.\n\n## Merchant reward — LanaPay deducts nothing\nYou receive the full invoice your customer paid, **plus a reward on top of it**: 2% as standard,\n5% for merchants enrolled in Lana8Wonder, and up to 20% for merchants in the Abundance model.\nThe reward is funded by the investor financing the purchase — never by you and never by the customer,\nwho pays exactly the invoice total. Nothing is paid out on failed or expired sales.\n\n## The two rules a till must never break\n1. The scanned customer QR is a **private key**. It goes from the scanner buffer into `customer.qr`\nof one HTTPS request and nowhere else — never into a log, a receipt journal, a database or a display.\n2. A `402 CUSTOMER_LANA_FAILED` answer means **no money moved — do not hand over the goods**\n(`do_not_hand_over_goods: true`). A timeout or 5xx is different: the outcome is unknown — recover\nwith `GET /api/v1/sales?order_id=…` before doing anything else, and never re-tender blindly.\n\nThere is **no sandbox**: creating, reading and cancelling sales moves no money and is free to\nexercise; the pay call is always real.\n\nFull prose documentation: https://pos.lanapays.us/llms-full.txt",
    "contact": {
      "name": "LanaPay POS",
      "url": "https://pos.lanapays.us/docs"
    }
  },
  "servers": [
    {
      "url": "https://pos.lanapays.us",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Sales",
      "description": "Create, inspect and cancel sales. order_id is the idempotency key."
    },
    {
      "name": "Payment",
      "description": "The synchronous pay call — the answer IS the outcome."
    },
    {
      "name": "Limits",
      "description": "Pre-flight: what this till can take right now, per rail."
    },
    {
      "name": "Public",
      "description": "Unauthenticated discovery data. Open CORS."
    }
  ],
  "security": [
    {
      "SecretKey": []
    }
  ],
  "paths": {
    "/api/v1/sales": {
      "post": {
        "tags": ["Sales"],
        "operationId": "createSale",
        "summary": "Create a sale (ring up the order)",
        "description": "`order_id` is the idempotency key. Re-posting the same order_id with an identical amount and currency returns the existing sale with **200** and `replayed: true` instead of creating a second one; a different amount is a 409 conflict. An expired or cancelled sale frees its order_id for a retry. The response includes `payment_options` so the till knows which rails this sale can take before offering tender.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["amount", "currency", "order_id"],
                "properties": {
                  "amount": {
                    "type": ["string", "number"],
                    "example": "19.90",
                    "description": "Fiat amount, at most 2 decimals, max 999999.99. A decimal string is preferred."
                  },
                  "currency": {
                    "type": "string",
                    "enum": ["EUR", "GBP", "USD"],
                    "description": "Must match the currency your business unit is bound to."
                  },
                  "order_id": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9._-]{1,64}$",
                    "example": "TILL1-0042",
                    "description": "Your till's own reference; the idempotency key."
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Shown in the dashboard and passed to the receipt description of the purchase record."
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string",
                      "maxLength": 200
                    },
                    "description": "Up to 10 keys, string values; echoed back on reads and webhooks."
                  },
                  "expires_in": {
                    "type": "integer",
                    "minimum": 300,
                    "maximum": 86400,
                    "description": "Seconds. Defaults to your merchant expiry setting (30 minutes unless changed)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Sale created.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Sale" }
              }
            }
          },
          "200": {
            "description": "Idempotent replay — the existing open sale for this order_id (`replayed: true`).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Sale" }
              }
            }
          },
          "400": {
            "description": "Validation failed: INVALID_AMOUNT, INVALID_CURRENCY, INVALID_ORDER_ID, INVALID_METADATA or INVALID_EXPIRES_IN (`error.param` names the field).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY — missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "403": {
            "description": "MERCHANT_PENDING, MERCHANT_SUSPENDED or MERCHANT_REJECTED — approval state blocks sale creation.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND — the key's unit is not enrolled or not active.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "409": {
            "description": "ORDER_ID_CONFLICT (same order_id, different amount/currency) or ORDER_ALREADY_PAID — `error.sale_id` points at the existing sale.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      },
      "get": {
        "tags": ["Sales"],
        "operationId": "listSales",
        "summary": "List sales",
        "description": "Newest first. `?order_id=…` (exact match) is the recovery query after a timeout: `paid` → fulfil, `pending` → repeat the same pay call, absent → re-create.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["pending", "paid", "cancelled", "expired"]
            },
            "description": "`pending` includes sales with a pay attempt in flight."
          },
          {
            "name": "order_id",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Exact order_id — the recovery filter."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Cursor: the last sale id of the previous page."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of sales.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SaleList" }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales/{id}": {
      "get": {
        "tags": ["Sales"],
        "operationId": "getSale",
        "summary": "Retrieve a sale",
        "description": "Fresh `payment_options` while the sale is pending. A pending sale past its expiry flips to `expired` on this read (lazy expiry).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "example": "sale_9f2kq7pxw4ln8vrt3a1b5c6d7e8f90ab"
          }
        ],
        "responses": {
          "200": {
            "description": "The sale.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Sale" }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND — unknown id, or another unit's sale (uniform on purpose: the API is not an existence oracle).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales/{id}/pay": {
      "post": {
        "tags": ["Payment"],
        "operationId": "paySale",
        "summary": "Pay a sale (the money call)",
        "description": "Synchronous — the answer IS the outcome; there is nothing to poll. `customer.qr` is the scanned customer card, passed through **verbatim as one opaque value**: it is interpreted server-side, used in memory, and never appears in any response, log or database row. Cash may come back with `payment.amount_adjusted` — the till MUST display and collect the adjusted amount; `payment.amount_charged` is always the authoritative figure. A `402 CUSTOMER_LANA_FAILED` means NO money moved: do not hand over the goods. On a timeout or 5xx the outcome is unknown — recover via `GET /api/v1/sales?order_id=…`, never by re-tendering. Rate limit: 30/min per key on top of the global 120/min.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["method", "customer"],
                "properties": {
                  "method": {
                    "type": "string",
                    "enum": ["cash", "crypto"],
                    "description": "`cash` records a cash purchase and mints the customer's cashback; `crypto` settles in LANA."
                  },
                  "customer": {
                    "type": "object",
                    "required": ["qr"],
                    "properties": {
                      "qr": {
                        "type": "string",
                        "maxLength": 200,
                        "description": "The scanned customer card, verbatim. OPAQUE — never parse, log, store or display it. Cash accepts a Lana card (WIF QR) or an L-address; crypto requires the card (WIF)."
                      },
                      "name": {
                        "type": "string",
                        "maxLength": 120,
                        "description": "Optional display name for the purchase record."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Paid — the sale with `payment` populated and `figures: \"actual\"` reward blocks. An already-paid sale replays with `replayed: true` (a double-tap is never a double charge).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Sale" }
              }
            }
          },
          "400": {
            "description": "INVALID_METHOD or CUSTOMER_QR_REQUIRED.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "402": {
            "description": "CUSTOMER_LANA_FAILED — **no money moved, do not hand over the goods** (`do_not_hand_over_goods: true`; the sale is pending again) — or INSUFFICIENT_CUSTOMER_BALANCE (the card cannot cover the sale).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "403": {
            "description": "MERCHANT_PENDING / MERCHANT_SUSPENDED / MERCHANT_REJECTED (unit blocked, both rails); MERCHANT_QUOTA_EXCEEDED, SPLIT_HAPPENING, CUSTOMER_WINDOW_EXCEEDED (cash — offer LANA; the window error carries `spent`, `limit`, `days`, `currency`); FUND_CAPACITY_EXCEEDED (crypto — carries `max_amount`, a block, never a silent reduction); WALLET_FROZEN / WALLET_NOT_REGISTERED / WALLET_CHECK_FAILED (customer card refused, fail-closed).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND — unknown id, or another unit's sale.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "409": {
            "description": "LANA_ONLY_UNIT (cash refused — offer crypto), MERCHANT_PAYOUT_NOT_CONFIGURED, SELF_PURCHASE, SALE_NOT_PAYABLE (expired/cancelled), PAYMENT_IN_PROGRESS (another attempt in flight — wait and re-poll), TOO_MANY_UTXOS (fragmented customer wallet) or RATE_CHANGED (rate moved twice — retry the same call).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "422": {
            "description": "UNSUPPORTED_QR_FORMAT (not a Lana card — rescan), WIF_REQUIRED_FOR_CRYPTO (an address cannot sign) or CUSTOMER_IDENTITY_UNRESOLVED (address-only scan of a wallet the registry holds no identity for).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "502": {
            "description": "UPSTREAM_UNAVAILABLE — the payment service was unreachable; the sale was NOT charged and is pending again. Retry the same pay call (a genuinely completed upstream payment self-heals into `replayed: true` via the order dedup).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales/{id}/cancel": {
      "post": {
        "tags": ["Sales"],
        "operationId": "cancelSale",
        "summary": "Cancel a pending sale",
        "description": "Ends a pending sale (fires the `sale.cancelled` webhook) and frees its order_id.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "The cancelled sale.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Sale" }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "409": {
            "description": "NOT_CANCELLABLE — the sale is paid, expired, already cancelled, or a payment is in flight.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/v1/limits": {
      "get": {
        "tags": ["Limits"],
        "operationId": "getLimits",
        "summary": "Pre-flight limits",
        "description": "What this till can take right now, before the basket is rung up: the effective cash maximum (merchant limit ∧ fund capacity ∧ default ∧ remaining monthly quota) and per-rail availability. The per-customer window still applies at pay time — treat `max_amount` as the ceiling, not a promise.",
        "parameters": [
          {
            "name": "currency",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["EUR", "GBP", "USD"]
            },
            "description": "Defaults to the unit's currency."
          }
        ],
        "responses": {
          "200": {
            "description": "The limits.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Limits" }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND — the key's unit is not known.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/v1/public/config": {
      "get": {
        "tags": ["Public"],
        "operationId": "getPublicConfig",
        "summary": "Public configuration",
        "security": [],
        "description": "No auth, open CORS. Currencies, live exchange rates and the reward percentages (what the merchant **earns** on top of the invoice — `fee_percent_*` are deprecated aliases of the same numbers).",
        "responses": {
          "200": {
            "description": "Current configuration.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PublicConfig" }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "sale.paid": {
      "post": {
        "summary": "A sale was paid — cash recorded or LANA settled on-chain",
        "description": "The till already received the synchronous pay answer; this event feeds the back office / ERP. Verify the `LanaPay-Signature` header (HMAC-SHA256 over `${t}.${rawBody}` with your `whsec_…` secret) against the RAW request bytes, dedupe on the event `id`, and respond 2xx fast. Delivery is at-least-once with retries at +1m, +5m, +30m, +2h, +6h, +24h, then dead.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WebhookEvent" }
            }
          }
        },
        "responses": {
          "200": { "description": "Any 2xx acknowledges the delivery." }
        }
      }
    },
    "sale.cancelled": {
      "post": {
        "summary": "A pending sale was cancelled via API or dashboard",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WebhookEvent" }
            }
          }
        },
        "responses": {
          "200": { "description": "Any 2xx acknowledges the delivery." }
        }
      }
    },
    "sale.expired": {
      "post": {
        "summary": "A pending sale ran out of time — no money moved",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WebhookEvent" }
            }
          }
        },
        "responses": {
          "200": { "description": "Any 2xx acknowledges the delivery." }
        }
      }
    },
    "test.ping": {
      "post": {
        "summary": "Dashboard 'Send test' — a signed event with a synthetic sale and \"test\": true",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WebhookEvent" }
            }
          }
        },
        "responses": {
          "200": { "description": "Any 2xx acknowledges the delivery." }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "SecretKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sk_live_…",
        "description": "Merchant secret API key, created in the dashboard and shown once. Scoped to one business unit — the key alone identifies the shop. Server-side only; /api/v1 deliberately has no CORS."
      }
    },
    "schemas": {
      "Sale": {
        "type": "object",
        "description": "A fiat-priced sale. The LANA amount is computed at payment time from the live exchange rate — never stored up front.",
        "properties": {
          "id": {
            "type": "string",
            "example": "sale_9f2kq7pxw4ln8vrt3a1b5c6d7e8f90ab"
          },
          "object": {
            "type": "string",
            "const": "sale"
          },
          "status": {
            "type": "string",
            "enum": ["pending", "paid", "expired", "cancelled"],
            "description": "A sale with a pay attempt in flight reports `pending`; a concurrent pay gets 409 PAYMENT_IN_PROGRESS."
          },
          "amount": {
            "type": "string",
            "example": "19.90",
            "description": "Invoice amount as a fixed 2-decimal string — never a float."
          },
          "currency": {
            "type": "string",
            "enum": ["EUR", "GBP", "USD"]
          },
          "order_id": {
            "type": "string",
            "example": "TILL1-0042"
          },
          "description": {
            "type": ["string", "null"]
          },
          "metadata": {
            "type": ["object", "null"],
            "additionalProperties": { "type": "string" }
          },
          "payment_options": {
            "oneOf": [
              { "$ref": "#/components/schemas/PaymentOptions" },
              { "type": "null" }
            ],
            "description": "Fresh while pending; null once terminal."
          },
          "payment": {
            "oneOf": [
              { "$ref": "#/components/schemas/Payment" },
              { "type": "null" }
            ],
            "description": "null until paid."
          },
          "merchant_reward": {
            "$ref": "#/components/schemas/RewardBlock",
            "description": "What the merchant EARNS on top of the invoice — funded by the investor financing the purchase; nothing is deducted from the sale itself."
          },
          "customer_cashback": {
            "$ref": "#/components/schemas/RewardBlock",
            "description": "What the customer gets back in LANA."
          },
          "created_at": { "type": "string" },
          "expires_at": { "type": ["string", "null"] },
          "paid_at": { "type": ["string", "null"] },
          "cancelled_at": { "type": ["string", "null"] },
          "replayed": {
            "type": "boolean",
            "description": "Present (true) when this response is an idempotent replay — a re-created open order or a re-paid sale."
          }
        }
      },
      "PaymentOptions": {
        "type": "object",
        "description": "Per-rail availability, computed from heartbeat-fresh mirrors. Every guard is re-checked authoritatively at pay time — an over-promise here costs one refused attempt, never money.",
        "properties": {
          "cash": {
            "type": "object",
            "properties": {
              "available": { "type": "boolean" },
              "max_amount": {
                "type": ["string", "null"],
                "example": "120.00",
                "description": "Largest cash amount this unit can currently take — the clamp target. null = no limit configured."
              },
              "reasons": {
                "type": "array",
                "items": { "$ref": "#/components/schemas/OptionReason" }
              }
            }
          },
          "crypto": {
            "type": "object",
            "properties": {
              "available": { "type": "boolean" },
              "currencies": {
                "type": "array",
                "description": "Indicative only — the settlement recomputes from the live rate at pay time.",
                "items": {
                  "type": "object",
                  "properties": {
                    "code": { "type": "string", "const": "LANA" },
                    "amount_lanoshis": { "type": ["integer", "null"] },
                    "rate": { "type": ["number", "null"] },
                    "rate_updated_at": { "type": ["string", "null"] }
                  }
                }
              },
              "reasons": {
                "type": "array",
                "items": { "$ref": "#/components/schemas/OptionReason" }
              }
            }
          }
        }
      },
      "OptionReason": {
        "type": "string",
        "enum": [
          "lana_only_unit",
          "merchant_quota_exceeded",
          "split_happening",
          "merchant_pending",
          "merchant_suspended",
          "merchant_rejected",
          "no_fund_capacity",
          "merchant_payout_not_configured",
          "no_rate"
        ],
        "description": "Why a rail is unavailable."
      },
      "Payment": {
        "type": "object",
        "description": "The payment result — everything a receipt needs.",
        "properties": {
          "method": {
            "type": "string",
            "enum": ["cash", "crypto"]
          },
          "currency_code": {
            "type": ["string", "null"],
            "description": "\"LANA\" on the crypto rail, null for cash."
          },
          "amount_charged": {
            "type": "string",
            "example": "19.90",
            "description": "The authoritative charged amount — print this, never the original."
          },
          "amount_adjusted": {
            "type": ["object", "null"],
            "description": "Set when a cash amount was clamped to the customer's remaining allowance. The till MUST display and collect the adjusted amount.",
            "properties": {
              "original": { "type": "string", "example": "75.00" },
              "adjusted": { "type": "string", "example": "50.00" },
              "currency": { "type": "string" }
            }
          },
          "tx_id": { "type": ["string", "null"] },
          "tx_hash": {
            "type": ["string", "null"],
            "description": "LanaCoin on-chain transaction hash (crypto rail)."
          },
          "lana_lanoshis": { "type": ["integer", "null"] },
          "exchange_rate": {
            "type": ["number", "null"],
            "description": "Fiat per LANA at pay time."
          },
          "customer_wallet": { "type": ["string", "null"] },
          "paid_at": { "type": ["string", "null"] }
        }
      },
      "RewardBlock": {
        "type": "object",
        "properties": {
          "figures": {
            "type": "string",
            "enum": ["indicative", "actual"],
            "description": "indicative = computed from the current policy/rate before payment; actual = snapshotted from the real settlement. Print only actual figures."
          },
          "percent": { "type": ["number", "null"] },
          "amount": { "type": "string", "example": "0.40" },
          "currency": { "type": "string" }
        }
      },
      "SaleList": {
        "type": "object",
        "properties": {
          "object": { "type": "string", "const": "list" },
          "data": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Sale" }
          },
          "has_more": { "type": "boolean" }
        }
      },
      "Limits": {
        "type": "object",
        "properties": {
          "currency": { "type": "string" },
          "max_amount": {
            "type": ["number", "null"],
            "description": "Effective per-transaction cash maximum (a number here, unlike sale amounts). null = no limit configured."
          },
          "source": {
            "type": "string",
            "enum": ["merchant", "fund", "default", "none"]
          },
          "merchant_limit": { "type": ["number", "null"] },
          "fund_limit": { "type": ["number", "null"] },
          "default_limit": { "type": ["number", "null"] },
          "cash_available": { "type": "boolean" },
          "crypto_available": { "type": "boolean" },
          "reasons": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/OptionReason" }
          }
        }
      },
      "PublicConfig": {
        "type": "object",
        "properties": {
          "currencies": {
            "type": "array",
            "items": { "type": "string" }
          },
          "reward_percent_base": {
            "type": "number",
            "description": "What the merchant EARNS on top of the invoice, standard tier."
          },
          "reward_percent_lana8wonder": {
            "type": "number",
            "description": "The reward for merchants enrolled in Lana8Wonder."
          },
          "reward_percent_max": {
            "type": "number",
            "description": "The Abundance-model ceiling."
          },
          "fee_percent_base": {
            "type": "number",
            "deprecated": true,
            "description": "Deprecated alias of reward_percent_base."
          },
          "fee_percent_lana8wonder": {
            "type": "number",
            "deprecated": true,
            "description": "Deprecated alias of reward_percent_lana8wonder."
          },
          "exchange_rates": {
            "type": "object",
            "additionalProperties": { "type": "number" }
          },
          "rates_updated_at": { "type": ["string", "null"] },
          "brain_reachable": { "type": "boolean" }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "description": "The frozen event payload. Headers: LanaPay-Signature (t=<unix>,v1=<hmac>), LanaPay-Event, LanaPay-Delivery-Id (equals the event id — the dedupe key).",
        "properties": {
          "id": {
            "type": "string",
            "example": "evt_2b91c4d8f7a35e604c1d9e8b0a7f6352"
          },
          "object": { "type": "string", "const": "event" },
          "type": {
            "type": "string",
            "enum": ["sale.paid", "sale.cancelled", "sale.expired", "test.ping"]
          },
          "created_at": { "type": "string" },
          "data": {
            "type": "object",
            "properties": {
              "sale": { "$ref": "#/components/schemas/Sale" }
            }
          },
          "test": {
            "type": "boolean",
            "description": "Present (true) only on test.ping."
          }
        }
      },
      "ErrorCode": {
        "type": "string",
        "description": "Every error code the API can answer with. See the errors section of the prose docs for the full cashier-facing table.",
        "enum": [
          "INVALID_API_KEY",
          "INVALID_AMOUNT",
          "INVALID_CURRENCY",
          "INVALID_ORDER_ID",
          "INVALID_METADATA",
          "INVALID_EXPIRES_IN",
          "INVALID_METHOD",
          "CUSTOMER_QR_REQUIRED",
          "UNSUPPORTED_QR_FORMAT",
          "WIF_REQUIRED_FOR_CRYPTO",
          "CUSTOMER_IDENTITY_UNRESOLVED",
          "MERCHANT_PENDING",
          "MERCHANT_SUSPENDED",
          "MERCHANT_REJECTED",
          "LANA_ONLY_UNIT",
          "MERCHANT_QUOTA_EXCEEDED",
          "SPLIT_HAPPENING",
          "MERCHANT_PAYOUT_NOT_CONFIGURED",
          "WALLET_FROZEN",
          "WALLET_NOT_REGISTERED",
          "WALLET_CHECK_FAILED",
          "SELF_PURCHASE",
          "CUSTOMER_WINDOW_EXCEEDED",
          "FUND_CAPACITY_EXCEEDED",
          "INSUFFICIENT_CUSTOMER_BALANCE",
          "ORDER_ID_CONFLICT",
          "ORDER_ALREADY_PAID",
          "SALE_NOT_PAYABLE",
          "PAYMENT_IN_PROGRESS",
          "NOT_CANCELLABLE",
          "TOO_MANY_UTXOS",
          "RATE_CHANGED",
          "CUSTOMER_LANA_FAILED",
          "NOT_FOUND",
          "UPSTREAM_UNAVAILABLE"
        ]
      },
      "Error": {
        "type": "object",
        "description": "The error envelope. Extra fields ride inside `error` where the cashier can act: `param` (validation), `sale_id` (order collisions), `spent`/`limit`/`days`/`currency` (CUSTOMER_WINDOW_EXCEEDED), `max_amount` (FUND_CAPACITY_EXCEEDED), `do_not_hand_over_goods` (CUSTOMER_LANA_FAILED). In rare cases a LANA payment can surface a typed refusal from the settlement layer outside the ErrorCode enum (an unshaped upstream 4xx becomes UPSTREAM_REJECTED) — treat any unknown code like UPSTREAM_UNAVAILABLE: nothing was charged; recover by order_id.",
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "anyOf": [
                  { "$ref": "#/components/schemas/ErrorCode" },
                  { "type": "string", "pattern": "^[A-Z0-9_]+$", "description": "Rare settlement-layer passthrough outside the enum — treat like UPSTREAM_UNAVAILABLE." }
                ]
              },
              "message": {
                "type": "string",
                "description": "Developer-facing English. Translate the code, not the message, for the cashier."
              },
              "param": { "type": "string" },
              "sale_id": { "type": "string" },
              "request_id": {
                "type": "string",
                "example": "req_5f2a9c1b3e7d4a08",
                "description": "Quote this when contacting support."
              },
              "do_not_hand_over_goods": {
                "type": "boolean",
                "description": "CUSTOMER_LANA_FAILED only: no money moved — the goods stay behind the counter."
              },
              "max_amount": { "type": "number" },
              "spent": { "type": "number" },
              "limit": { "type": "number" },
              "days": { "type": "integer" },
              "currency": { "type": "string" }
            }
          }
        }
      }
    }
  }
}
