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.