# SDKs

URL: https://zerodev.ca/docs/kora/developer-documentation/sdks

> Check license keys, activate devices, read offline licenses and manage licenses, products and customers from Node.js, Java, Python, FiveM, .NET, PHP, Go or Rust.



The SDKs do two things:

* Check a license key against `POST /api/v1/licenses/validate`, and optionally verify the response signature.
* Take and free device seats with `POST /api/v1/licenses/activate` and `POST /api/v1/licenses/deactivate`.
* Download signed offline license files and check them without a network call.
* Manage licenses, products and customers through `/api/v1/licenses`, `/api/v1/products` and `/api/v1/customers`, including customer merge.

The source is in [zerodev-ca/Kora-SDKs](https://github.com/zerodev-ca/Kora-SDKs).

| Platform                                  | Package                                          | Version |
| ----------------------------------------- | ------------------------------------------------ | ------- |
| Node.js and TypeScript                    | `@zerodev-ca/kora`                               | 3.1.1   |
| Java 17+ (Paper, Spigot, Velocity, Folia) | `ca.zerodev:kora-java`                           | 3.1.1   |
| Python 3.8+                               | `zerodev-kora`                                   | 3.1.1   |
| FiveM and RedM                            | `kora-fivem`                                     | 3.1.1   |
| .NET 8                                    | `ZeroDev.Kora`                                   | 3.1.1   |
| PHP 8.1+ with `sodium`                    | `zerodev/kora`                                   | 3.1.1   |
| Go 1.21+                                  | `github.com/zerodev-ca/Kora-SDKs/packages/go/v3` | 3.1.1   |
| Rust                                      | `zerodev-kora`                                   | 3.1.1   |

A [Paper plugin template](https://github.com/zerodev-ca/Kora-SDKs/tree/main/templates/paper) has the license check, seat activation and an offline fallback built in. Copy it and add your own code.

<Callout type="info">
  The **Integration** page in the dashboard shows your request URL and public key, SDK and HTTP snippets filled in with your own address and products, and a tester that runs a real validation.
</Callout>

## Authentication

Validation is public unless **Require an API token with the validate scope** is on in the Licenses config. Management calls always need an API token from the **API Tokens** page, sent as `Authorization: Bearer <token>`, with the matching scopes (`licenses.view`, `licenses.create`, `products.edit` and so on).

## Validation Response

```json
{
    "valid": true,
    "code": "valid",
    "message": "This license is valid.",
    "license": {
        "key": "XXXXX-XXXXX-XXXXX-XXXXX",
        "status": "active",
        "product": { "id": 1, "name": "Test Product 1", "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
}
```

`code` is one of `valid`, `invalid_request`, `not_found`, `missing_product`, `wrong_product`, `disabled`, `expired`, `inactive`, `leaked`, `not_whitelisted`, `seats_full`, `blacklisted`, `blocked`, `unauthorized`, or one of your negative statuses. See [Result Codes](/docs/kora/extensions/core#result-codes). `license` is `null` unless the key is valid. `license.policy` is the policy the key was issued under, or `null`. Check its `name` to unlock features per plan. The `x-kora-signature` header is an Ed25519 signature of the raw response body, checked against the key from `GET /api/v1/public-key`.

## Devices and Offline Licenses

|                              | Node.js                            | Java                                         | Python                      | FiveM                                             |
| ---------------------------- | ---------------------------------- | -------------------------------------------- | --------------------------- | ------------------------------------------------- |
| Extra fields such as `hwid`  | `validate(key, product, { hwid })` | `validate(key, product, Map.of("hwid", id))` | `validate(key, hwid=id)`    | `config.fields`                                   |
| Take a seat and validate     | `activate(key, product, fields)`   | `activate(key, fields)`                      | `activate(key, **fields)`   | `activate(config, key, callback)`                 |
| Free a seat                  | `deactivate(key, fields)`          | `deactivate(key, fields)`                    | `deactivate(key, **fields)` | `deactivate(config, key, callback)`               |
| Download a license file      | `file(id)`                         | `file(id)`                                   | `file(id)`                  |                                                   |
| Check a license file offline | `offline(file)`                    | `offline(file)`                              | `offline(file)`             | `offline(config, file, callback)`                 |
| Merge customers              | `merge(id, duplicateId)`           | `merge(id, duplicateId)`                     | `merge(id, duplicate_id)`   | `customers(config, "merge", { id, customer_id })` |

|                              | .NET                                  | PHP                         | Go                             | Rust                              |
| ---------------------------- | ------------------------------------- | --------------------------- | ------------------------------ | --------------------------------- |
| Validate                     | `ValidateAsync(key, product, fields)` | `validate($key, $fields)`   | `Validate(ctx, key, fields)`   | `validate(&key, &[("hwid", id)])` |
| Take a seat and validate     | `ActivateAsync(key, product, fields)` | `activate($key, $fields)`   | `Activate(ctx, key, fields)`   | `activate(&key, fields)`          |
| Free a seat                  | `DeactivateAsync(key, fields)`        | `deactivate($key, $fields)` | `Deactivate(ctx, key, fields)` | `deactivate(&key, fields)`        |
| Download a license file      | `FileAsync(id)`                       | `file($id)`                 | `File(ctx, id)`                | `file(id)`                        |
| Check a license file offline | `Offline(file)`                       | `offline($file)`            | `Offline(file)`                | `offline(&file)`                  |
| Merge customers              | `MergeAsync(id, duplicateId)`         | `merge($id, $duplicateId)`  | `Merge(ctx, id, duplicateID)`  | `merge(id, duplicate_id)`         |

`offline` needs the public key, verifies the signature and refuses files whose `refresh_after` or `expires_at` has passed. Validate online first and read the file only when Kora cannot be reached. See [Offline Licenses](/docs/kora/extensions/core#offline-licenses).

## Node.js

```bash
npm install @zerodev-ca/kora
```

```ts
import { Client } from "@zerodev-ca/kora";

const kora = new Client({ url: "https://kora.example.com", product: "Test Product 1", publicKey: process.env.KORA_PUBLIC_KEY });

const result = await kora.validate(licenseKey);
if (!result.valid) throw new Error(result.message);
```

```ts
const admin = new Client({ url: "https://kora.example.com", apiKey: process.env.KORA_TOKEN });

const product = await admin.products.create({ name: "Test Product 1", version: "1.0.0" });
const customer = await admin.customers.create({ name: "Test Customer 1", email: "customer1@example.com" });
const license = await admin.licenses.create({ product_id: product.id, customer_id: customer.id });

const { total, rows } = await admin.licenses.list({ status: "active", limit: 25 });
await admin.licenses.update(license.id, { status: "suspended" });
await admin.licenses.delete(license.id);
```

Failed management calls throw a `KoraError` with `status`.

## Java

```xml
<dependency>
    <groupId>ca.zerodev</groupId>
    <artifactId>kora-java</artifactId>
    <version>3.1.1</version>
</dependency>
```

```java
KoraClient kora = new KoraClient.Builder()
    .url("https://kora.example.com")
    .product("Test Product 1")
    .publicKey(publicKeyPem)
    .build();

KoraResponse result = kora.validate(licenseKey);
if (!result.isValid()) {
    getLogger().severe(result.getMessage());
    getServer().getPluginManager().disablePlugin(this);
}
```

`kora.licenses()`, `kora.products()` and `kora.customers()` return a `KoraResource` with `list`, `get`, `create`, `update` and `delete`, working on Gson `JsonObject` values. Failures throw `KoraException` with `getStatus()`.

## Python

```bash
pip install zerodev-kora
```

```python
from kora import Client

kora = Client("https://kora.example.com", product="Test Product 1", public_key=public_key_pem)

result = kora.validate(license_key)
if not result.valid:
    raise SystemExit(result.message)

admin = Client("https://kora.example.com", api_key=token)
license = admin.licenses.create(product_id=1, customer_id=4)
page = admin.licenses.list(status="active")
```

Failures raise `KoraError` with `status`.

## FiveM

Copy `kora-fivem` into your resources and `ensure kora-fivem` before your own resource.

```lua
local config = { url = "https://kora.example.com", product = "Test Product 1" }

exports["kora-fivem"]:validate(config, GetConvar("license_key", ""), function(valid, data)
    if not valid then
        print("^1" .. data.message .. "^7")
        StopResource(GetCurrentResourceName())
    end
end)
```

`licenses`, `products` and `customers` take an action (`list`, `get`, `create`, `update`, `delete`) and an arguments table with `id`, `fields`, `query` or `force`:

```lua
exports["kora-fivem"]:licenses({ url = "https://kora.example.com", apiKey = GetConvar("kora_token", "") }, "list", { query = { status = "active" } }, function(success, page)
    if success then print(page.total) end
end)
```

## .NET

```bash
dotnet add package ZeroDev.Kora
```

```csharp
using Kora;

using var kora = new KoraClient("https://kora.example.com", product: "Test Product 1", publicKey: publicKeyPem);

var result = await kora.ValidateAsync(licenseKey);
if (!result.Valid) throw new Exception(result.Message);
```

`kora.Licenses`, `kora.Products` and `kora.Customers` have `ListAsync`, `GetAsync`, `CreateAsync`, `UpdateAsync` and `DeleteAsync`. Failures throw `KoraException` with `Status`.

## PHP

```bash
composer require zerodev/kora
```

```php
use ZeroDev\Kora\Client;

$kora = new Client("https://kora.example.com", product: "Test Product 1", publicKey: $publicKeyPem);

$result = $kora->validate($licenseKey);
if (!$result["valid"]) {
    exit($result["message"]);
}
```

`$kora->licenses`, `$kora->products` and `$kora->customers` have `list`, `get`, `create`, `update` and `delete`. Failures throw `KoraException` with `status`.

## Go

```bash
go get github.com/zerodev-ca/Kora-SDKs/packages/go/v3
```

```go
client := kora.New(kora.Options{URL: "https://kora.example.com", Product: "Test Product 1", PublicKey: publicKeyPem})

result, err := client.Validate(context.Background(), licenseKey, nil)
if err != nil {
    log.Fatal(err)
}
if !result.Valid {
    log.Fatal(result.Message)
}
```

`client.Licenses`, `client.Products` and `client.Customers` have `List`, `Get`, `Create`, `Update` and `Delete`. Failed calls return a `*kora.Error` with `Status`.

## Rust

```bash
cargo add zerodev-kora
```

```rust
let kora = kora::Client::new("https://kora.example.com").product("Test Product 1").public_key(public_key_pem);

let result = kora.validate(&license_key, &[])?;
if !result.valid {
    return Err(result.message.into());
}
```

`kora.licenses()`, `kora.products()` and `kora.customers()` have `list`, `get`, `create`, `update` and `delete`. Failed calls return `Error::Status(status, message)`.
