Kora/developer documentation/Core Data

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);
MethodWhat 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

FieldOn issueWhat it does
product or product_idRequiredA product ID or name.
customer or customer_idRequiredA customer ID, Discord ID or email.
policy or policy_idNoneA policy ID or name from the same product. Throws a 400 if it belongs to another product.
keyGeneratedYour own key. Throws a 409 if it is taken.
statusFirst statusOne of the configured statuses.
addonsPolicy's addonsAddon IDs. IDs from other products are dropped.
max_machinesPolicy's value, else 0Seats in each whitelist group. 0 uses each group's own limit.
expires_atPolicy or product defaultA date, or "" to use the policy's duration, or the product's without a policy.
leakedtrue 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:

FieldWhat it is
statusThe 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_nameFor tables.
addons, addon_namesAddon IDs and their names.
policy, policy_name{ id, name, type } and the name, or null.
leakedWhether a leak was flagged.
whitelistsWhitelisted values by group.

Statuses

MethodReturns
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" });
MethodWhat 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.
FieldWhat it is
name, description, versionText.
priceRounded to two decimals.
currencyOne of products.currencies. Defaults to USD.
duration, duration_unitHow long licenses last. 0 is lifetime. Units are days, weeks, months or years.
activefalse makes every key for it fail with disabled.
offlineBoolean. false disables offline license file exports for this product. Defaults to true.
whitelist_groupsWhitelist 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" });
MethodWhat 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.
FieldWhat it is
typesubscription or perpetual, from policies.types. Perpetual policies always have a duration of 0.
max_machinesSeats in each whitelist group for licenses issued under it. 0 uses each group's own limit.
duration, duration_unitSame as on products.
addonsAddon 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 });
MethodWhat 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

HandlerUse it for
addonscreate, update, remove and details(ids) for addons.
whitelistsGroups (createGroup, attach to a product), entries (add, remove, reset, forLicense) and seats (activate, deactivate).
rulesAccess rules: create, update, remove, and match(productId, { body, ip, headers }), which returns { blocked, excluded }.
validationsrecord 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.