Files

3.8 KiB

API reference

All endpoints are JSON in / JSON out. Errors return a body of the form:

{ "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.

{
  "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:

{
  "product": "my-app",
  "buyer_email": "alice@example.com",
  "buyer_note": "optional",
  "redirect_url": "https://myapp.example.com/thanks"
}

Response:

{
  "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:

{
  "invoice_id": "...",
  "status": "pending",
  "product_id": "...",
  "amount_sats": 50000,
  "license_key": null,
  "license_id": null
}

Once settled:

{
  "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:

{
  "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):

{ "ok": true, "license_id": "...", "product_id": "...", "product_slug": "my-app", "issued_at": "..." }

On failure:

{ "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

{
  "slug": "my-app",
  "name": "My App",
  "description": "...",
  "price_sats": 50000,
  "metadata": { "anything": "useful" }
}

PATCH /v1/admin/products/:id/active

Activate or deactivate a product.

{ "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.

{ "product_slug": "my-app", "note": "comp for @alice" }

Response:

{
  "license_id": "...",
  "product_id": "...",
  "license_key": "LIC1-...-...",
  "issued_at": "..."
}

POST /v1/admin/licenses/:id/revoke

{ "reason": "chargeback" }

Idempotent: revoking an already-revoked license returns 404.