Core Data
Issue, find and change licenses, products and customers from your own module.
Licenses, products, policies and customers belong to the core module. Work with them through its handlers rather than the models, so keys, defaults, addons and events are handled the same way the dashboard does.
get licenses() {
return this.kora.modules.handlers.get("core", "licenses");
}Add "core" to your manifest's requires so your module only loads when core is on.
Licenses
const license = await this.licenses.issue({ product: "Test Product 1", customer: "[email protected]" });
await this.licenses.update(license.key, { status: "suspended" });
await this.licenses.remove(license.key);| Method | What it does |
|---|---|
issue(input) | Create a license. Returns it described. |
update(reference, input) | Change a license. Returns it described. |
remove(reference) | Delete a license. |
resolve(reference) | Find by ID or key, or return null. |
require(reference) | The same, but throws a 404 when it is missing. |
describe(rows) | Add product, customer, addons and whitelist details to one row or a list. |
file(reference) | A signed offline license file as { license, filename, content }. Throws a 409 for licenses that do not pass validation, and a 403 when offline files are disabled globally or for the product. |
deactivate(body, ip, headers) | Free the seats matching the request, like POST /api/v1/licenses/deactivate. |
A reference is a license ID or its key. Keys are trimmed and upper-cased before matching.
Input
| Field | On issue | What it does |
|---|---|---|
product or product_id | Required | A product ID or name. |
customer or customer_id | Required | A customer ID, Discord ID or email. |
policy or policy_id | None | A policy ID or name from the same product. Throws a 400 if it belongs to another product. |
key | Generated | Your own key. Throws a 409 if it is taken. |
status | First status | One of the configured statuses. |
addons | Policy's addons | Addon IDs. IDs from other products are dropped. |
max_machines | Policy's value, else 0 | Seats in each whitelist group. 0 uses each group's own limit. |
expires_at | Policy or product default | A date, or "" to use the policy's duration, or the product's without a policy. |
leaked | true flags a leak, false clears it and restarts the count. |
Changing a license's product keeps only the addons that also belong to the new product, and drops its policy unless you send one for the new product. Changing the policy on an existing license does not copy its defaults again. Moving an expired license's expiry into the future sets it back to the first passing status.
Described Licenses
describe, issue and update return rows with extra fields:
| Field | What it is |
|---|---|
status | The effective status. A passing license past its expiry and grace period reads expired. |
product | { id, name, version, active, whitelist_groups }. |
customer | { id, name, email, discord_id }. |
product_name, customer_name | For tables. |
addons, addon_names | Addon IDs and their names. |
policy, policy_name | { id, name, type } and the name, or null. |
leaked | Whether a leak was flagged. |
whitelists | Whitelisted values by group. |
Statuses
| Method | Returns |
|---|---|
statuses() | Every status as { value, label, valid, message }, with expired last. |
status(value) | One status. |
names() | Every status value. |
passing() | The values of statuses that pass validation. |
options() | { value, label } pairs for a select. |
generate() | A new key in the configured format. Use unique() for one not already taken. |
Validating a Key
const result = await this.licenses.validate({ key, product: "Test Product 1", hwid }, ip, headers, false);
if (!result.valid) return this.fail(403, result.message);This runs the same checks as the public validation endpoint, including access rules and blocked customers, writes a validation log entry, runs leak detection and fires license.validated. Pass true as the last argument to take free whitelist seats first, like POST /api/v1/licenses/activate. result has the same shape as the API response, without the signature.
Products
const products = this.kora.modules.handlers.get("core", "products");
const product = await products.create({ name: "Test Product 4", price: 9.99, currency: "USD", duration: 30, duration_unit: "days" });| Method | What it does |
|---|---|
create(input) | Create a product. name is required. |
update(id, input) | Change a product. |
remove(id, force) | Delete a product. Throws a 409 while it has licenses, unless force is true, which deletes them too. Its addons and policies are always deleted with it. |
resolve(reference) | Find by ID or exact name. |
expiry(product) | The expiry date a new license would get. |
| Field | What it is |
|---|---|
name, description, version | Text. |
price | Rounded to two decimals. |
currency | One of products.currencies. Defaults to USD. |
duration, duration_unit | How long licenses last. 0 is lifetime. Units are days, weeks, months or years. |
active | false makes every key for it fail with disabled. |
offline | Boolean. false disables offline license file exports for this product. Defaults to true. |
whitelist_groups | Whitelist group IDs. |
Policies
const policies = this.kora.modules.handlers.get("core", "policies");
const policy = await policies.create({ product_id: product.id, name: "Pro", type: "subscription", max_machines: 2, duration: 365, duration_unit: "days" });
await this.licenses.issue({ product: product.id, customer: "[email protected]", policy: "Pro" });| Method | What it does |
|---|---|
create(input) | Create a policy. product_id and name are required. |
update(id, input) | Change a policy. Moving it to another product unlinks its licenses. |
remove(id) | Delete a policy. Its licenses keep their values and lose the link. |
resolve(reference, productId) | Find a policy of that product by ID or exact name. |
describe(rows) | Add product_name and the licenses count to one row or a list. |
| Field | What it is |
|---|---|
type | subscription or perpetual, from policies.types. Perpetual policies always have a duration of 0. |
max_machines | Seats in each whitelist group for licenses issued under it. 0 uses each group's own limit. |
duration, duration_unit | Same as on products. |
addons | Addon IDs of the same product. |
Customers
const customers = this.kora.modules.handlers.get("core", "customers");
const found = await customers.resolve(discordId);
const customer = found ? found : await customers.create({ discord_id: discordId, name: username });| Method | What it does |
|---|---|
create(input) | Create a customer from name, email, discord_id, notes and blocked. Without a name, the part of the email before the @ is used. |
update(id, input) | Change a customer. |
remove(id, force) | Delete a customer. Throws a 409 while they own licenses, unless force is true. |
resolve(reference) | Find by ID, Discord ID or email. |
merge(id, duplicateId) | Move the duplicate's licenses to the customer, fill in missing details, delete the duplicate and fire customer.merged. |
A customer with blocked: true fails validation with blocked on every license they own.
Other Handlers
| Handler | Use it for |
|---|---|
addons | create, update, remove and details(ids) for addons. |
whitelists | Groups (createGroup, attach to a product), entries (add, remove, reset, forLicense) and seats (activate, deactivate). |
rules | Access rules: create, update, remove, and match(productId, { body, ip, headers }), which returns { blocked, excluded }. |
validations | record a validation yourself and prune old logs. |
Errors
Handler methods throw with a status and a plain message, such as a 404 for a missing license. Inside a route you can let them through, and the client gets that status with { error: message }. Elsewhere, catch them:
try {
await this.licenses.update(key, { status: "revoked" });
} catch (error) {
this.kora.console.log(this.kora.console.levels.warning, `Could not revoke ${key}: ${error.message}`);
}Events
Every change fires an event with the described record, such as license.created with { license, input }. See Events for the full list.