# Overview

URL: https://zerodev.ca/docs/kora/extensions/core

> Licenses, products, policies, customers, addons, whitelists and access rules, plus the endpoints your product calls to check a key.



Core is the licensing part of Kora. It is always on and every other module builds on it.

## Records

| Record          | What it is                                                                                                              |
| --------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Product         | Something you sell. Has a version, price, license duration and the whitelist groups it uses.                            |
| Policy          | A named variant of a product, such as "Pro". Holds the type, max machines, duration and addons a new license gets.      |
| Addon           | An extra sold on top of a product, such as a source code tier.                                                          |
| Customer        | Who owns a license. Name, email, Discord ID and notes. Can be blocked.                                                  |
| License         | A key for one product and one customer. Can carry a policy, addons, max machines, whitelist entries and an expiry date. |
| Whitelist group | A rule that limits where a key works, by IP, request header or request body field.                                      |
| Access rule     | Blocks or excludes an IP or range, a device or a country, for one product or all of them.                               |

Manage them from the dashboard, the [management API](/docs/kora/extensions/core/api-reference) or the [Discord bot](/docs/kora/extensions/discord).

## Checking a Key

Your product sends a `POST` to `/api/v1/licenses/validate` on the API port. The endpoint is public unless you turn on **Require an API token** in the [configuration](/docs/kora/extensions/core/configuration).

```json title="Request"
{
    "license_key": "ABCDE-FGHIJ-KLMNO-PQRST",
    "product_name": "My Plugin"
}
```

`product_name` takes the product's name or ID. Add `hwid` to log the device, and any fields your whitelist groups check.

```json title="Response"
{
    "valid": true,
    "code": "valid",
    "message": "This license is valid.",
    "license": {
        "key": "ABCDE-FGHIJ-KLMNO-PQRST",
        "status": "active",
        "product": { "id": 1, "name": "My Plugin", "version": "1.0.0", "active": true },
        "policy": { "id": 2, "name": "Pro", "type": "subscription" },
        "customer": { "id": 4, "name": "Test Customer 1", "email": "customer1@example.com", "discord_id": null },
        "addons": [],
        "expires_at": null,
        "whitelists": {}
    },
    "timestamp": 1791083116035
}
```

`license` is only filled in when the key is valid. `policy` is `null` for licenses issued without one. Each IP can make 60 requests a minute.

### Result Codes

| Code              | Meaning                                                                     |
| ----------------- | --------------------------------------------------------------------------- |
| `valid`           | The key works.                                                              |
| `invalid_request` | No key was sent.                                                            |
| `not_found`       | The key does not exist.                                                     |
| `missing_product` | No product was sent and the configuration requires one.                     |
| `wrong_product`   | The key belongs to a different product.                                     |
| `disabled`        | The product is turned off.                                                  |
| `expired`         | The license has expired.                                                    |
| `inactive`        | The license has a status Kora does not know.                                |
| `leaked`          | The key was used in too many places. See [Leak Detection](#leak-detection). |
| `not_whitelisted` | The request's IP, header or field is not on the license.                    |
| `seats_full`      | Activation found no free seat in a whitelist group.                         |
| `blacklisted`     | An access rule blocks the request.                                          |
| `blocked`         | The license's customer is blocked.                                          |
| `unauthorized`    | An API token is required and none was sent.                                 |
| A status ID       | The license has a negative status, such as `suspended` or `revoked`.        |

Change the message for each code on the **Lang** page under **Validation messages**.

## Signed Responses

Every validation response is signed with an Ed25519 key. The signature is in the `x-kora-signature` header, base64 encoded, over the exact response body. Checking it stops someone from faking a "valid" answer with a fake server.

Get the public key from **Settings > Response Signing Key** or from `GET /api/v1/public-key`, and put it in your product.

```js
import crypto from "crypto";

const response = await fetch("https://api.kora.example.com/api/v1/licenses/validate", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ license_key: key, product_name: "My Plugin" })
});
const body = await response.text();
const trusted = crypto.verify(null, Buffer.from(body), crypto.createPublicKey(publicKey), Buffer.from(response.headers.get("x-kora-signature"), "base64"));
const valid = trusted && JSON.parse(body).valid;
```

The [SDKs](/docs/kora/developer-documentation/sdks) check the signature for you when you give them the public key.

<Callout type="warn">
  **Rotate Key** creates a new key pair. Every build that has the old public key stops trusting Kora until you update it.
</Callout>

## Policies

A policy is a named variant of a product, such as "Pro" or "Lifetime". Create policies on the **Policies** page, then pick one when you issue a license.

| Field        | What it does                                                                                             |
| ------------ | -------------------------------------------------------------------------------------------------------- |
| Product      | The product the policy belongs to. A license can only use policies of its own product.                   |
| Name         | Shown on the license and returned in the validation response as `license.policy`.                        |
| Type         | **Subscription** licenses expire after the duration. **Perpetual** licenses never expire.                |
| Max Machines | How many seats the license gets in each whitelist group of the product. `0` uses each group's own limit. |
| Duration     | How long a subscription lasts, in days, weeks, months or years. `0` is lifetime.                         |
| Addons       | Addons of the same product that a new license starts with.                                               |

Issuing a license under a policy copies these values onto the license. Anything you fill in yourself wins, so you can still give one customer an extra machine or a later expiry. Validation only reads the license's own values, so editing a policy later changes new licenses but leaves issued ones alone. Deleting a policy unlinks its licenses and keeps their values.

## Whitelists

A whitelist group limits where a license works. Attach groups to a product, then add entries to each license.

| Type   | Checks                                       |
| ------ | -------------------------------------------- |
| IP     | The address the request came from.           |
| Header | A request header, such as `X-Server-Id`.     |
| Body   | A field in the request body, such as `hwid`. |

The header or field name defaults to the group name. When a product has groups, every request must match an entry on the license for each group, or it gets `not_whitelisted`.

Entries are added by staff on the license, by customers on the [Client Dashboard](/docs/kora/client-dashboard), with `/addwhitelist` in Discord, or by your product through activation. **Max Entries per License** caps how many each license can hold. A license with **Max Machines** set uses that number instead for every group.

## Activation

Each whitelist entry is a seat. Your product can manage its own seat instead of waiting for staff:

| Endpoint                           | What it does                                                                                                                                      |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /api/v1/licenses/activate`   | Adds the request's values to every group of the product that still has a free seat, then validates. Fails with `seats_full` when a group is full. |
| `POST /api/v1/licenses/deactivate` | Removes the request's values from the license. Answers `deactivated`, `not_activated` or `not_found`.                                             |

Both take the same body as validation and are signed. Turn on **Activate new devices on validation** in the configuration to make every validation activate, so the first check from a new device takes a seat.

```json title="Deactivate response"
{
    "success": true,
    "code": "deactivated",
    "message": "This device was deactivated.",
    "removed": 1,
    "timestamp": 1791083116035
}
```

## Offline Licenses

Every license with a positive status can be exported as a signed file, so your product keeps working without a network or while Kora is down. Use **Offline File** on the license, `GET /api/v1/licenses/:id/file` with an API token, or let customers download their own on the [Client Dashboard](/docs/kora/client-dashboard).

The file is `base64url(payload) + "." + base64url(signature)`. The signature is Ed25519 over the exact payload bytes, checked with the same public key as signed responses.

| Payload field                                 | Meaning                                                                                             |
| --------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `key`, `license_id`, `status`                 | The license.                                                                                        |
| `product`, `customer`, `addons`, `whitelists` | What it is for and where it may run.                                                                |
| `expires_at`                                  | The license expiry, or `null`.                                                                      |
| `issued_at`                                   | When the file was made.                                                                             |
| `refresh_after`                               | Stop trusting the file after this time. The configured number of days, or `expires_at` when sooner. |

Validate online first. Fall back to the file only when Kora cannot be reached, and reject it once `refresh_after` or `expires_at` has passed. A revoked license keeps working offline until then, which is the trade-off for working without a network.

## Access Rules

**Access Rules** block or exclude requests before a license is judged.

| Type        | Matches                                                                                                                                                                          |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| IP or range | The request IP, as a single address or CIDR range such as `203.0.113.0/24`. IPv6 works too.                                                                                      |
| Device      | The `hwid` field of the request.                                                                                                                                                 |
| Country     | A two-letter code. Kora reads the country header (`cf-ipcountry` by default) when a proxy such as Cloudflare sets it, and otherwise looks the IP up in its bundled geo database. |

The geo database ships with Kora in `modules/core/resources/geo`, built from [DB-IP IP to Country Lite](https://db-ip.com) (CC BY 4.0). With **Update the geo database monthly** on, Kora downloads the newest release on the 3rd of each month into `data/core/geo` and uses it from then on. Lookups happen on your server; no IP leaves it.

A **Block** rule fails the request with `blacklisted` and counts the hit. An **Exclude** rule lets the request skip whitelist seats and leak detection, so you can test your own builds without using a seat. **Exclude My IP** adds a rule for the address you are browsing from. Pick a product to limit a rule to it, or leave it empty for every product. Block wins when both match.

## Customers

**Block** a customer to fail every license they own with `blocked`, without touching the license statuses. **Unblock** brings them all back as they were, and licenses issued while blocked are blocked too.

**Merge** folds a duplicate into the customer you have open: its licenses move over, a missing email or Discord ID is filled in, notes are joined, the block stays if either was blocked, and the duplicate is deleted. BuiltByBit buyers and purchases move with it.

## Expiry

A product's duration sets the expiry for new licenses. `0` means lifetime. Kora marks licenses as expired every 10 minutes, after the grace period in the configuration.

## Validation Logs

Every request is logged with its key, product, result, IP, country and device. Countries come from the same lookup as country rules. Find them under **Validation Logs**, or on each license. Logs older than the retention setting are removed every hour.

## Leak Detection

When a key is checked from more IPs or devices than allowed within the window, not counting requests that matched an exclude rule, Kora flags it and returns `leaked`. Flagged licenses show under **Overview**. Set the action to **Flag and change status** to also give the license a negative status such as **Suspended**.

To clear a flag, edit the license and set **Leak** to **Clear**.
