# License Runtime API

The runtime API is for client applications that need to validate, activate, and deactivate generated server-validated licenses.

Supported MVP mode:

- `generated_server_validated`

Not supported by these runtime endpoints yet:

- `imported_csv`
- `offline_signed_ed25519_future`

The server normalizes the submitted license key, calculates an HMAC lookup hash, and point-reads the issued license record. It does not scan license collections and does not use reversible encryption or key pattern checks as proof of validity.

Device fingerprints are sensitive. Send a stable app-defined fingerprint, but do not include buyer personal data. The backend stores only an HMAC hash of the fingerprint.

## Validate

```bash
curl -sS https://api.example.test/v1/licenses/validate \
  -H 'Content-Type: application/json' \
  -d '{
    "product_id": "prod_123",
    "license_key": "LIC1-XXXX-XXXX-XXXX-XXXX"
  }'
```

## Activate

```bash
curl -sS https://api.example.test/v1/licenses/activate \
  -H 'Content-Type: application/json' \
  -d '{
    "product_id": "prod_123",
    "license_key": "LIC1-XXXX-XXXX-XXXX-XXXX",
    "device_fingerprint": "app-specific-device-fingerprint"
  }'
```

Activation is idempotent for the same license and same device fingerprint. A different device is rejected with `409 conflict` when the product `activation_limit` is exceeded.

## Deactivate

```bash
curl -sS https://api.example.test/v1/licenses/deactivate \
  -H 'Content-Type: application/json' \
  -d '{
    "product_id": "prod_123",
    "license_key": "LIC1-XXXX-XXXX-XXXX-XXXX",
    "device_fingerprint": "app-specific-device-fingerprint"
  }'
```

Deactivation marks the activation inactive and frees one seat. Repeated deactivation is idempotent.

Products must allow deactivation in their policy for this endpoint to free seats.

## TypeScript

```ts
type LicenseResponse = {
  valid: boolean;
  status: string;
  activation_limit: number;
  activation_count: number;
  features?: string[];
  expires_at?: string;
};

export async function activateLicense(apiBase: string, productId: string, licenseKey: string, deviceFingerprint: string): Promise<LicenseResponse> {
  const res = await fetch(`${apiBase}/v1/licenses/activate`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      product_id: productId,
      license_key: licenseKey,
      device_fingerprint: deviceFingerprint,
    }),
  });
  if (!res.ok) throw new Error(`license activation failed: ${res.status}`);
  return res.json() as Promise<LicenseResponse>;
}
```

## Go

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
)

type licenseRequest struct {
	ProductID         string `json:"product_id"`
	LicenseKey        string `json:"license_key"`
	DeviceFingerprint string `json:"device_fingerprint,omitempty"`
}

type licenseResponse struct {
	Valid           bool     `json:"valid"`
	Status          string   `json:"status"`
	ActivationLimit int      `json:"activation_limit"`
	ActivationCount int      `json:"activation_count"`
	Features        []string `json:"features,omitempty"`
}

func activate(apiBase, productID, licenseKey, deviceFingerprint string) (licenseResponse, error) {
	body, _ := json.Marshal(licenseRequest{ProductID: productID, LicenseKey: licenseKey, DeviceFingerprint: deviceFingerprint})
	resp, err := http.Post(apiBase+"/v1/licenses/activate", "application/json", bytes.NewReader(body))
	if err != nil {
		return licenseResponse{}, err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return licenseResponse{}, fmt.Errorf("license activation failed: %s", resp.Status)
	}
	var out licenseResponse
	return out, json.NewDecoder(resp.Body).Decode(&out)
}
```

## Python

```python
import requests


def activate_license(api_base: str, product_id: str, license_key: str, device_fingerprint: str) -> dict:
    response = requests.post(
        f"{api_base}/v1/licenses/activate",
        json={
            "product_id": product_id,
            "license_key": license_key,
            "device_fingerprint": device_fingerprint,
        },
        timeout=10,
    )
    response.raise_for_status()
    return response.json()
```

## Response Notes

Responses do not include raw license keys, encrypted license keys, device fingerprints, buyer personal data, or payment data.

`validate` does not increment `activation_count`. Only `activate` and `deactivate` write activation state.
