Skip to content

Here's the doc #14

Description

@j2l

Problem

No doc for humans 😄

Note: The (partially generated) doc below might have mistakes.

Proposed solution

Overview

Keygate is a license management API for software products. This document covers the public SDK endpoints and portal endpoints. The API uses a consistent JSON envelope for all responses.

Base URL: {server}/api/v1

Install

(see in readme)

First Admin run

Into the .env:

  • Add an admin email into ADMIN_EMAILS
  • If no SMTP is set up, validation code is in your keygate container's logs.

docker compose up -d
Visit http://localhost:9000
Create a product (desktop, saas or hybrid).
Create a plan for the product.
In another browser, register a test customer (needs working SMTP settings in your .env)
Back into the admin:
check for the customer
Issue a new license for this customer

Customer Example Workflow

SDK Integration (Mobile App)

First Customer Run:

    # Get public key
    curl https://your-server.com/api/v1/license/pubkey
    # Save the public_key for offline verification

Activation:

    curl -X POST https://your-server.com/api/v1/license/activate \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: my-unique-id" \
      -d '{
        "license_key": "KG-...",
        "identifier": "device-uuid",
        "label": "iPhone 15"
      }'

On Each Launch:

    curl -X POST https://your-server.com/api/v1/license/verify \
      -H "Content-Type: application/json" \
      -d '{
        "license_key": "KG-...",
        "identifier": "device-uuid"
      }'
    # Verify the returned token offline using saved public key

Usage Recording:

    curl -X POST https://your-server.com/api/v1/license/usage \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: usage-20260820-001" \
      -d '{
        "license_key": "KG-...",
        "feature": "api_calls",
        "quantity": 1
      }'

Floating License

Checkout:

    curl -X POST https://your-server.com/api/v1/license/floating/checkout \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: checkout-001" \
      -d '{
        "license_key": "KG-...",
        "identifier": "engineering-01",
        "label": "Engineer Workstation"
      }'

Heartbeat every 15 minutes:

    curl -X POST https://your-server.com/api/v1/license/floating/heartbeat \
      -H "Content-Type: application/json" \
      -d '{
        "license_key": "KG-...",
        "identifier": "engineering-01"
      }'

Checkin when done:

    curl -X POST https://your-server.com/api/v1/license/floating/checkin \
      -H "Content-Type: application/json" \
      -d '{
        "license_key": "KG-...",
        "identifier": "engineering-01"
      }'

Authentication & Security

SDK Public Endpoints

Public SDK endpoints (/license/*, /releases/:slug/feed.*) do NOT require an API key. The license_key in the request body is the credential. Stable release feeds are fully public — trust comes from the per-product Ed25519 signature on each artifact, not from URL secrecy.

Admin Server-to-Server

For server-to-server admin access (CI/CD, billing backends, automation scripts) use API keys with the admin scope. Pass them as:

text
Authorization: Bearer kg_live_…

Admin keys are equivalent to a logged-in admin session and can call any /admin/* endpoint. Mint them in the admin dashboard under API Keys.

Portal Endpoints

Portal endpoints (/portal/*) require a session cookie issued by /auth/otp/verify, /auth/dev-login, or /invites/accept. The cookie is HttpOnly + SameSite=Lax; browsers attach it automatically.


Idempotency

Write endpoints (/license/activate, /license/usage, /license/floating/checkout) honor the Idempotency-Key header:

  • Same key + same body: Replays the cached response (200/4xx) within 24 hours

  • Same key + different body: Returns 422 IDEMPOTENCY_KEY_CONFLICT

  • Concurrent retries while original is still running: Returns 409 IDEMPOTENCY_IN_FLIGHT (retry with backoff)

Replayed idempotent responses carry the header Idempotent-Replayed: true.


Response Format

Success Response

json
{
  "success": true,
  "data": {
    // Response data
  }
}

Error Response

json
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "...",
    "details": { ... }
  }
}

Endpoints

License Management

Get Verification Key

GET /license/pubkey

Returns the Ed25519 public key used to verify offline tokens.

Request Example:

http
GET /api/v1/license/pubkey
Host: your-server.com

Response Example:

json
{
  "success": true,
  "data": {
    "algorithm": "ed25519",
    "format": "hex",
    "public_key": "a1b2c3d4e5f678901234567890abcdef1234567890abcdef1234567890abcdef"
  }
}

Activate License

POST /license/activate

Registers a device or user identifier against a license. Returns a signed verification token.

Request Headers:

  • Idempotency-Key (optional, string, max 256 chars)

Request Body:

json
{
  "license_key": "KG-ABCD1234-EFGH5678-IJKL9012-MNOP3456",
  "identifier": "device-a1b2c3d4",
  "identifier_type": "device",
  "label": "MacBook Pro"
}

Response Example:

json
{
  "success": true,
  "data": {
    "status": "activated",
    "license_id": "550e8400-e29b-41d4-a716-446655440000",
    "token": "eyJhbGciOiJIUzI1NiIsInR...<base64url-payload>.<base64url-signature>"
  }
}

Error Responses:

  • 400: Validation failure

  • 403: License not usable (expired/suspended/revoked/canceled)

  • 404: License not found

  • 409: ACTIVATION_LIMIT (slots exhausted) or IDEMPOTENCY_IN_FLIGHT

  • 422: IDEMPOTENCY_KEY_CONFLICT

  • 429: Brute-force lockout


Verify License

POST /license/verify

Checks that a license is valid and the identifier is activated. Returns the license status, plan features, and a signed token.

Request Body:

json
{
  "license_key": "KG-ABCD1234-EFGH5678-IJKL9012-MNOP3456",
  "identifier": "device-a1b2c3d4"
}

Response Example:

json
{
  "success": true,
  "data": {
    "status": "active",
    "plan_id": "plan_123",
    "plan_name": "Pro",
    "valid_until": "2027-01-01T00:00:00Z",
    "features": {
      "premium_support": true,
      "api_calls": "50000",
      "storage_gb": "100"
    },
    "token": "eyJhbGciOiJIUzI1NiIsInR...",
    "grace_days": 0
  }
}

Error Response:

json
{
  "success": false,
  "error": {
    "code": "LICENSE_NOT_FOUND",
    "message": "License not found or invalid",
    "details": null
  }
}

Note: All "license-knowable" failures collapse to 404 LICENSE_NOT_FOUND to prevent probing.


Deactivate License

POST /license/deactivate

Removes an activation, freeing a slot for a new device.

Request Body:

json
{
  "license_key": "KG-ABCD1234-EFGH5678-IJKL9012-MNOP3456",
  "identifier": "device-a1b2c3d4"
}

Response Example:

json
{
  "success": true,
  "data": null
}

Error Response:

json
{
  "success": false,
  "error": {
    "code": "LICENSE_NOT_FOUND",
    "message": "License not found or identifier not activated",
    "details": null
  }
}

Entitlements

Check Feature Entitlements

POST /license/entitlements

Returns all features available for the license's plan, including quota usage for metered features.

Request Body:

json
{
  "license_key": "KG-ABCD1234-EFGH5678-IJKL9012-MNOP3456",
  "feature": "premium_support"  // Optional
}

Response Example:

json
{
  "success": true,
  "data": {
    "licensed": true,
    "status": "active",
    "plan_id": "plan_123",
    "plan_name": "Pro",
    "features": {
      "premium_support": {
        "enabled": true,
        "value_type": "bool",
        "value": "true",
        "used": null,
        "limit": null,
        "remaining": null,
        "period": "lifetime",
        "resets_at": null
      },
      "api_calls": {
        "enabled": true,
        "value_type": "quota",
        "value": "50000",
        "used": 451,
        "limit": 1000,
        "remaining": 549,
        "period": "monthly",
        "resets_at": "2026-04-01T00:00:00Z"
      }
    }
  }
}

Usage

Record Usage

POST /license/usage

Increments a usage counter for a metered feature. Atomic — concurrent requests will never exceed the quota.

Request Headers:

  • Idempotency-Key (optional)

Request Body:

json
{
  "license_key": "KG-ABCD1234-EFGH5678-IJKL9012-MNOP3456",
  "feature": "api_calls",
  "quantity": 5,
  "metadata": {
    "endpoint": "/users",
    "method": "GET"
  }
}

Response Example:

json
{
  "success": true,
  "data": {
    "accepted": true,
    "used": 456,
    "limit": 1000,
    "remaining": 544,
    "period": "monthly",
    "period_key": "2026-03"
  }
}

Error Response:

json
{
  "success": false,
  "error": {
    "code": "QUOTA_EXCEEDED",
    "message": "Usage quota exceeded for api_calls",
    "details": {
      "used": 1000,
      "limit": 1000,
      "period": "monthly"
    }
  }
}

Get Quota Status

POST /license/usage/status

Returns current usage and remaining quota for a feature.

Request Body:

json
{
  "license_key": "KG-ABCD1234-EFGH5678-IJKL9012-MNOP3456",
  "feature": "api_calls"
}

Response Example:

json
{
  "success": true,
  "data": {
    "feature": "api_calls",
    "used": 456,
    "limit": 1000,
    "remaining": 544,
    "period": "monthly",
    "period_key": "2026-03"
  }
}

Alternatives considered

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions