# API reference All endpoints are JSON in / JSON out. Errors return a body of the form: ```json { "ok": false, "error": "not_found", "message": "product 'xyz'" } ``` Admin endpoints require `Authorization: Bearer $KEYSAT_ADMIN_API_KEY`. --- ## Public endpoints ### `GET /` Service metadata including the Ed25519 public key. Useful for SDKs to fetch the key at build time. ```json { "service": "keysat", "version": "0.2.0", "operator": "Acme Software", "public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCow...\n-----END PUBLIC KEY-----\n", "key_algorithm": "ed25519", "key_format_version": 1 } ``` ### `GET /healthz` Liveness probe. Returns `{"ok": true}`. ### `GET /v1/pubkey` Just the public key. ### `GET /v1/products` List all active products. ### `GET /v1/products/:slug` Single product by slug. ### `POST /v1/purchase` Start a purchase. Request: ```json { "product": "my-app", "buyer_email": "alice@example.com", "buyer_note": "optional", "redirect_url": "https://myapp.example.com/thanks" } ``` Response: ```json { "invoice_id": "uuid-of-our-row", "btcpay_invoice_id": "...", "checkout_url": "https://btcpay.example.com/i/...", "amount_sats": 50000, "poll_url": "https://license.example.com/v1/purchase/uuid-of-our-row" } ``` ### `GET /v1/purchase/:invoice_id` Poll for license delivery. While pending: ```json { "invoice_id": "...", "status": "pending", "product_id": "...", "amount_sats": 50000, "license_key": null, "license_id": null } ``` Once settled: ```json { "invoice_id": "...", "status": "settled", "product_id": "...", "amount_sats": 50000, "license_key": "LIC1-...-...", "license_id": "..." } ``` ### `POST /v1/validate` The hot path. Downstream software calls this at startup (and on a cadence) to check revocation. Request: ```json { "key": "LIC1-...-...", "product_slug": "my-app", "fingerprint": "sha256-of-some-installation-unique-data" } ``` `product_slug` and `fingerprint` are optional. If `fingerprint` is provided and the license row has no fingerprint bound yet, the first caller's fingerprint is locked to the license (trust-on-first-use). Later callers presenting a different fingerprint are rejected with `reason: "fingerprint_mismatch"`. Response (always HTTP 200 so middleware doesn't log these as errors): ```json { "ok": true, "license_id": "...", "product_id": "...", "product_slug": "my-app", "issued_at": "..." } ``` On failure: ```json { "ok": false, "reason": "revoked" } ``` Possible `reason` values: `bad_format`, `bad_signature`, `not_found`, `revoked`, `suspended`, `expired`, `product_mismatch`, `fingerprint_mismatch`, `too_many_machines` (multi-seat cap reached). ### `POST /v1/btcpay/webhook` Landing point for BTCPay Server webhook events. Only BTCPay should call this. We verify `BTCPay-Sig` HMAC before trusting anything. --- ## Admin endpoints All of these require `Authorization: Bearer $KEYSAT_ADMIN_API_KEY`. ### `POST /v1/admin/products` ```json { "slug": "my-app", "name": "My App", "description": "...", "price_sats": 50000, "metadata": { "anything": "useful" } } ``` ### `PATCH /v1/admin/products/:id/active` Activate or deactivate a product. ```json { "active": false } ``` Deactivated products are hidden from public listings and reject new purchases; existing licenses continue to validate. ### `GET /v1/admin/licenses?product_id=...` List licenses for a product. ### `POST /v1/admin/licenses` Manually issue a license outside the purchase flow — for comps, press keys, developer testing. ```json { "product_slug": "my-app", "note": "comp for @alice" } ``` Response: ```json { "license_id": "...", "product_id": "...", "license_key": "LIC1-...-...", "issued_at": "..." } ``` ### `POST /v1/admin/licenses/:id/revoke` ```json { "reason": "chargeback" } ``` Idempotent: revoking an already-revoked license returns 404.