# License API Reference

This document describes all HTTP endpoints exposed by the CoForen License Center.

**All endpoints are under the `/license/api/coforen/` prefix.**

---

## Base Information

- **Base URL**: `https://foren.dev/license/api/coforen/`
- **Content-Type**: `application/json` for request and response bodies
- **Encoding**: UTF-8
- **Timestamps**: UTC ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`)
- **HTTP Status Codes**:
  - `200` — Success
  - `400` — Invalid input
  - `401` — Missing/invalid API token
  - `404` — Resource not found / token expired/used
  - `405` — Wrong HTTP method
  - `429` — Rate limit exceeded
  - `500` — Server error

## Authentication

Most endpoints require a **Bearer API token** in the `Authorization` header:

```
Authorization: Bearer f7c4...e2d1
```

Some endpoints are **public** (no auth required) — typically read-only or token-based operations. See the [Authentication Matrix](#authentication-matrix) below.

### Getting an API Token

1. Log in to the admin console at `/admin/login.php`
2. Navigate to **API Tokens** in the sidebar
3. Click **+ Generate Token**
4. Choose a name (e.g., "Production CoForen Client")
5. Set an expiry (or leave blank for no expiry)
6. **Copy the token immediately** — it is shown only once
7. Store it securely on the client side (e.g., in a config file, not in version control)

The token is hashed (SHA-256) before storage. Plaintext cannot be recovered.

### Token Verification

For each authenticated request:
1. Token is extracted from `Authorization: Bearer <token>`
2. SHA-256 hash is computed
3. Database lookup verifies the hash + the credentials are active and not expired
4. Lookup is rate-limited per minute (configurable via `security.api_rate_limit_per_minute`)

## Response Format

All responses are JSON:

```json
{
  "ok": true|false,
  ... endpoint-specific fields ...
}
```

Error responses always include:

```json
{
  "ok": false,
  "error_code": "snake_case_error_identifier",
  "message": "Human-readable description"
}
```

---

## Authentication Matrix

| Endpoint | Method | Auth Required | Rate Limit |
|---|---|---|---|
| [`/health/`](#health) | GET | No | Yes (60/min) |
| [`/public-key/`](#public-key) | GET | No | Yes (60/min) |
| [`/activate/`](#activate) | POST | No | Yes (30/min) |
| [`/validate-offline/`](#validate-offline) | POST | No | Yes (30/min) |
| [`/authorize/`](#authorize) | POST | No | Yes (60/min) |
| [`/authorize/token/`](#authorize-token) | POST | No | Yes (60/min) |
| [`/certs/validate/`](#certs-validate) | POST | No | Yes (60/min) |
| [`/certs/crl/`](#certs-crl) | GET | No | Yes (60/min) |
| [`/certs/generate/`](#certs-generate) | POST | **Yes** | Yes (60/min) |
| [`/certs/revoke/`](#certs-revoke) | POST | **Yes** | Yes (60/min) |
| [`/certs/download-key/`](#certs-download-key) | GET | Token-based | Yes (60/min) |
| [`/renew/`](#renew) | POST | **Yes** | Yes (60/min) |
| [`/upgrade/`](#upgrade) | POST | **Yes** | Yes (60/min) |
| [`/plans/`](#plans) | GET/POST/PUT/DELETE | **Yes** (write ops only) | Yes (60/min) |
| [`/stats/`](#stats) | GET | **Yes** | Yes (30/min) |
| [`/webhooks/`](#webhooks) | GET/DELETE | **Yes** | Yes (60/min) |
| [`/webhooks/register/`](#webhooks-register) | POST | **Yes** | Yes (60/min) |

---

## Endpoints

### `health`

`GET /license/api/coforen/health/`

Liveness/readiness check. Returns database connectivity status. **No auth required.**

**Response 200**:

```json
{
  "ok": true,
  "service": "foren-license-coforen",
  "db": "ok",
  "public_key_ready": true,
  "time": "2026-09-29T12:00:00Z"
}
```

**Curl**:

```bash
curl https://foren.dev/license/api/coforen/health/
```

---

### `public-key`

`GET /license/api/coforen/public-key/`

Returns the server's Ed25519 public key for license signature verification. **No auth required.**

**Response 200**:

```json
{
  "ok": true,
  "service": "foren-license-coforen",
  "algorithm": "Ed25519",
  "public_key": "MCowBQYDK2VwAyEA...base64...",
  "v2_view_key_fingerprint": "sha256:a1b2c3...",  // null if FOREN_LICENSE_V2_VIEW_KEY is not configured
  "time": "2026-09-29T12:00:00+00:00"
}
```

> **Note**: `v2_view_key_fingerprint` is `null` if `FOREN_LICENSE_V2_VIEW_KEY` is not configured. In that case v2-signed license envelopes cannot be issued.

Clients should cache this key and use it to verify any signed license payload returned by the server.

**Curl**:

```bash
curl https://foren.dev/license/api/coforen/public-key/
```

---

### `activate`

`POST /license/api/coforen/activate/`

Activate a license on a device. Returns a signed license payload that the client verifier can validate offline.

**No auth required** — rate limited.

**Request Body**:

```json
{
  "activation_code": "ACT-CODE-HERE",
  "machine_fingerprint": "sha256:abc123...64hex",
  "product": "CoForen",
  "version": "0.8.1",
  "client_version": "0.8.1",
  "device_name": "Investigator Laptop",
  "os": "macOS 14.0"
}
```

| Field | Required | Validation |
|---|---|---|
| `activation_code` | Yes | Activation code assigned to license holder |
| `machine_fingerprint` | Yes | Format `sha256:[a-f0-9]{64}` |
| `product` | No | Default `CoForen` (sent but not currently enforced by the activation service) |
| `version` | No | Default `0.8.1` |
| `device_name` | No | Free text |
| `os` | No | Free text |
| `license_key` | Ignored | Included for backward compatibility only; the server derives the license from the activation code, not this field |

**Response 200 (Success)**:

```json
{
  "ok": true,
  "status": 200,
  "license": {
    "license_id": 2,
    "license_key": "COF-20260521-CC30C5",
    "product": "CoForen",
    "version": "0.8.1",
    "holder": "Beta",
    "plan": "team",
    "features": ["case_map","lan_collaboration","file_upload"],
    "max_members": 6,
    "max_cases": 50,
    "issued_at": "2026-05-21T10:00:00Z",
    "expires_at": "2027-05-21T10:00:00Z",
    "machine_fingerprint": "sha256:abc...",
    "grace_days_remaining": 7
  },
  "signed_payload": "base64-encoded-signed-json...",
  "public_key": "MCowBQYDK2VwAyEA..."
}
```

**Response 400** examples:

- `{"ok": false, "status": 400, "message": "activation_code and machine_fingerprint are required"}`
- `{"ok": false, "status": 400, "message": "invalid machine_fingerprint"}`
- `{"ok": false, "status": 400, "message": "invalid machine_code"}`

**Response 403 (revoked/expired)**:

```json
{"ok": false, "status": 403, "message": "License is expired or revoked"}
```

**Curl**:

```bash
curl -X POST https://foren.dev/license/api/coforen/activate/ \
  -H 'Content-Type: application/json' \
  -d '{
    "activation_code": "ACT-XXXX-XXXX",
    "machine_fingerprint": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
    "product": "CoForen"
  }'
```

---

### `validate-offline`

`POST /license/api/coforen/validate-offline/`

Validate a license file (offline verifier). Checks signature, expiry, revocation status, and grace period.

**No auth required** — rate limited.

**Request Body**:

```json
{
  "license_text": "base64-encoded-license-data..."
}
```

**Response 200**:

```json
{
  "ok": true,
  "valid": true,
  "license_id": 2,
  "expires_at": "2027-05-21T10:00:00Z",
  "grace_days_remaining": 0,
  "in_grace_period": false,
  "is_expired": false,
  "is_revoked": false,
  "is_grace_period": false
}
```

---

### `authorize`

`POST /license/api/coforen/authorize/`

Verify that a license is authorized for a specific product. Used by clients to check feature gates.

**No auth required** — rate limited.

**Request Body**:

```json
{
  "license_key": "COF-20260521-CC30C5",
  "product_slug": "CoForen",
  "feature": "case_map"  // optional
}
```

**Response 200**:

```json
{
  "ok": true,
  "authorized": true,
  "license_id": 2,
  "product_slug": "CoForen",
  "features": ["case_map","lan_collaboration", ...],
  "has_feature": true,
  "expires_at": "2027-05-21T10:00:00Z",
  "grace_days_remaining": 0,
  "is_expired": false,
  "is_grace_period": false
}
```

---

### `authorize-token`

`POST /license/api/coforen/authorize/token/`

Verify an authorization token (issued via admin) and check that it grants access to a product.

**No auth required** — rate limited.

**Request Body**:

```json
{
  "token": "auth-token-here",
  "product_slug": "CoForen"
}
```

**Response 200**:

```json
{
  "ok": true,
  "valid": true,
  "license_id": 2,
  "product_slug": "CoForen",
  "expires_at": "2027-05-21T10:00:00Z",
  "grace_days_remaining": 0,
  "is_in_grace_period": false
}
```

---

### `certs-generate`

`POST /license/api/coforen/certs/generate/`

Generate a self-signed X.509 client certificate for mTLS authentication. **API token required.**

**Request Body**:

```json
{
  "owner_name": "Zhang San",
  "owner_license_id": 2,
  "days_valid": 365
}
```

**Response 200**:

```json
{
  "ok": true,
  "serial": "abc123def456...",
  "cert_pem": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
  "key_download_token": "f7c4...e2d1",
  "key_download_url": "https://foren.dev/license/api/coforen/certs/download-key/?token=f7c4...",
  "key_download_expires_at": "2026-09-29T12:30:00Z",
  "expires_at": "2027-09-29T12:00:00Z"
}
```

> **Security**: The private key is **NEVER** returned in this response. Use the `key_download_url` to retrieve it via [`certs-download-key`](#certs-download-key). The download token is single-use and expires in 30 minutes.

**Curl**:

```bash
curl -X POST https://foren.dev/license/api/coforen/certs/generate/ \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"owner_name":"Zhang San","owner_license_id":2,"days_valid":365}'
```

---

### `certs-validate`

`POST /license/api/coforen/certs/validate/`

Validate a certificate. Checks X.509 validity period and database revocation status. **No auth required.**

**Request Body**:

```json
{
  "cert_pem": "-----BEGIN CERTIFICATE-----\n..."
}
```

**Response 200**:

```json
{
  "ok": true,
  "valid": true,
  "serial": "abc123...",
  "subject": ["CN=Zhang San", "O=ForenDev"],
  "issuer": ["O=ForenDev"],
  "not_before": "2026-09-29T12:00:00Z",
  "not_after": "2027-09-29T12:00:00Z"
}
```

---

### `certs-revoke`

`POST /license/api/coforen/certs/revoke/`

Revoke a certificate. **API token required.**

**Request Body**:

```json
{
  "serial": "abc123..."
}
```

**Response 200**:

```json
{"ok": true, "revoked": true, "serial": "abc123..."}
```

---

### `certs-download-key`

`GET /license/api/coforen/certs/download-key/?token=<token>`

Retrieve a private key using the one-time download token from [`certs-generate`](#certs-generate). The token is **single-use** and expires 30 minutes after issuance.

**No auth header required** — the token itself is the credential.

**Response 200**:

```json
{
  "ok": true,
  "key_pem": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----",
  "format": "PEM"
}
```

**Error responses**:

- `400 invalid_token` — Token format invalid
- `404 token_expired_or_used` — Token used or past 30-minute window

After the first successful call, the token is invalidated in the database.

---

### `certs-crl`

`GET /license/api/coforen/certs/crl/`

Get the Certificate Revocation List. **No auth required.**

**Response 200**:

```json
{
  "ok": true,
  "crl": [
    {"serial": "abc123...", "revoked_at": "2026-09-15T10:00:00Z", "reason": "requested"},
    ...
  ],
  "generated_at": "2026-09-29T12:00:00Z",
  "total": 5
}
```

---

### `renew`

`POST /license/api/coforen/renew/`

Renew an existing license by extending the expiry date. **API token required.**

**Request Body**:

```json
{
  "license_id": 2,
  "extend_days": 365,
  "new_expires_at": "2027-09-29T12:00:00Z"  // alternative to extend_days
}
```

**Response 200**:

```json
{
  "ok": true,
  "license_id": 2,
  "old_expires_at": "2026-09-29T12:00:00Z",
  "new_expires_at": "2027-09-29T12:00:00Z",
  "event_id": 42
}
```

---

### `upgrade`

`POST /license/api/coforen/upgrade/`

Upgrade a license to a higher plan. **API token required.**

**Request Body**:

```json
{
  "license_id": 2,
  "new_plan": "enterprise"
}
```

**Response 200**:

```json
{
  "ok": true,
  "license_id": 2,
  "old_plan": "team",
  "new_plan": "enterprise",
  "event_id": 43
}
```

---

### `plans`

`GET/POST/PUT/DELETE /license/api/coforen/plans/`

Manage license plans. **API token required** for write operations (POST/PUT/DELETE).

**GET** — list all plans:

**Response 200**:

```json
{
  "ok": true,
  "plans": [
    {
      "slug": "team",
      "name": "Team",
      "description": "Forensic teams up to 6 investigators",
      "features": ["case_map","lan_collaboration","file_upload","graph_annotations","export"],
      "max_members": 6,
      "max_cases": 50,
      "grace_days": 7
    },
    ...
  ]
}
```

**POST** — create plan:

```json
{
  "slug": "professional",
  "name": "Professional",
  "description": "...",
  "features": ["case_map", ...],
  "max_members": 15,
  "max_cases": 200,
  "grace_days": 14
}
```

**PUT** — update plan (same body as POST, all fields optional).

**DELETE** — delete plan. Returns 409 if licenses are still using it.

---

### `stats`

`GET /license/api/coforen/stats/?from=YYYY-MM-DD&to=YYYY-MM-DD`

Get usage statistics. **API token required.**

**Response 200**:

```json
{
  "ok": true,
  "period": {"from": "2026-09-01", "to": "2026-09-30"},
  "activations": {
    "total_activations": 84,
    "active_devices": 11,
    "success_rate": 95.2,
    "by_product": [{"product": "CoForen", "activations": 84, "devices": 11}, ...],
    "by_day": [{"date": "2026-09-01", "activations": 3}, ...]
  },
  "licenses": {
    "total": 20,
    "active": 15,
    "expired": 4,
    "expiring_soon": 1,
    "in_grace": 0,
    "by_plan": [{"plan": "team", "count": 12}, ...]
  }
}
```

---

### `webhooks`

`GET /license/api/coforen/webhooks/` — list all webhooks. **API token required.**

**DELETE /license/api/coforen/webhooks/?id=N** — delete webhook. **API token required.**

---

### `webhooks-register`

`POST /license/api/coforen/webhooks/register/`

Register a webhook to receive license events. **API token required.**

**Request Body**:

```json
{
  "url": "https://your-app.example.com/webhook",
  "scope": "all" | "license.activated" | "license.renewed" | etc.
}
```

**Response 200**:

```json
{
  "ok": true,
  "webhook_id": 5,
  "secret": "auto-generated-shared-secret"
}
```

> Save the `secret` — it's used to verify webhook signatures via `X-License-Webhook-Signature: sha256=<hmac>` header.

**Webhook delivery**: events are queued in `license_events` table. The `scripts/check_expiring_licenses.php` cron dispatches pending events. Failed events retry up to 5 times with exponential backoff.

---

## Rate Limiting

All endpoints enforce per-IP rate limits (defaults below; configurable in `license/config.php`):

| Endpoint group | Default limit |
|---|---|
| Activation | 30 / minute |
| Statistics | 30 / minute |
| All others | 60 / minute |

When exceeded, returns:

```json
{"ok": false, "error_code": "rate_limited", "message": "Too many requests"}
```

Status code: `429`.

---

## Versioning

This is **v1** of the License API. Breaking changes will be announced via a `Deprecation` header and a 6-month migration window.

## See Also

- [SECURITY.md](SECURITY.md) — Authentication, signature verification, security model
- [DEPLOYMENT.md](DEPLOYMENT.md) — Environment variables, database setup, web server configuration
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) — Common errors and debugging guide