Kora/extensions/Overview

Overview

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

RecordWhat it is
ProductSomething you sell. Has a version, price, license duration and the whitelist groups it uses.
PolicyA named variant of a product, such as "Pro". Holds the type, max machines, duration and addons a new license gets.
AddonAn extra sold on top of a product, such as a source code tier.
CustomerWho owns a license. Name, email, Discord ID and notes. Can be blocked.
LicenseA key for one product and one customer. Can carry a policy, addons, max machines, whitelist entries and an expiry date.
Whitelist groupA rule that limits where a key works, by IP, request header or request body field.
Access ruleBlocks 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 or the Discord bot.

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.

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.

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": "[email protected]", "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

CodeMeaning
validThe key works.
invalid_requestNo key was sent.
not_foundThe key does not exist.
missing_productNo product was sent and the configuration requires one.
wrong_productThe key belongs to a different product.
disabledThe product is turned off.
expiredThe license has expired.
inactiveThe license has a status Kora does not know.
leakedThe key was used in too many places. See Leak Detection.
not_whitelistedThe request's IP, header or field is not on the license.
seats_fullActivation found no free seat in a whitelist group.
blacklistedAn access rule blocks the request.
blockedThe license's customer is blocked.
unauthorizedAn API token is required and none was sent.
A status IDThe 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.

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 check the signature for you when you give them the public key.

Rotate Key creates a new key pair. Every build that has the old public key stops trusting Kora until you update it.

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.

FieldWhat it does
ProductThe product the policy belongs to. A license can only use policies of its own product.
NameShown on the license and returned in the validation response as license.policy.
TypeSubscription licenses expire after the duration. Perpetual licenses never expire.
Max MachinesHow many seats the license gets in each whitelist group of the product. 0 uses each group's own limit.
DurationHow long a subscription lasts, in days, weeks, months or years. 0 is lifetime.
AddonsAddons 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.

TypeChecks
IPThe address the request came from.
HeaderA request header, such as X-Server-Id.
BodyA 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, 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:

EndpointWhat it does
POST /api/v1/licenses/activateAdds 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/deactivateRemoves 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.

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.

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 fieldMeaning
key, license_id, statusThe license.
product, customer, addons, whitelistsWhat it is for and where it may run.
expires_atThe license expiry, or null.
issued_atWhen the file was made.
refresh_afterStop 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.

TypeMatches
IP or rangeThe request IP, as a single address or CIDR range such as 203.0.113.0/24. IPv6 works too.
DeviceThe hwid field of the request.
CountryA 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 (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.