The CLV1 protocol is now publicRead the spec
Browse the library

Documentation libraryGet started by language

Get started with Unity

Unity cannot load the C# SDK (that SDK needs .NET 8), so a Unity game calls the ClavisAUH C library through P/Invoke, the way Unity uses any native plugin. The same code runs on .NET Framework 4.x. This guide goes from installing the library to your game's build.

5 min read

What you need

  • Unity with the Mono or IL2CPP scripting backend, building for Windows x64 or Linux x64. The C library has no macOS, Android or iOS build yet.
  • An app on the dashboard. Its Integration › Connect software page has the API address, the App ID, the public key and a Make a test key button. The API address ends in /api, with no /v1: https://<your ClavisAUH site>/api.

1. Install the C library

Download from Downloads and check the SHA-256:

  • Windows: clavis-c-1.0.0-windows-x64.zip, with clavis.dll: one file that needs only Windows's own DLLs.
  • Linux: clavis-c-1.0.0-linux-x64.tar.gz, with libclavis.so.1. The machine running the game needs the system's libsodium and libcurl (Debian, Ubuntu: apt install libsodium23 libcurl4).

Put clavis.dll in your project's Assets/Plugins/x86_64/. For Linux, copy libclavis.so.1 into the same folder and rename it libclavis.so. In each file's Inspector, pick its platform (Windows x86_64 or Linux x86_64) for both the Editor and Standalone.

2. The minimal code

Create Assets/Scripts/Clavis.cs with the class below, as it is. It uses only DllImport and byte arrays for UTF-8 strings, so Mono and IL2CPP take it the same way:

static class Clavis
{
    const string Lib = "clavis";
    const CallingConvention Cdecl = CallingConvention.Cdecl;

    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_open(
        byte[] baseUrl, byte[] appId, byte[] appKey, byte[] stateDir, byte[] appVersion);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern void clavis_close(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_init(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_activate(IntPtr client, byte[] key);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_heartbeat(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_logout(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_offline(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_variable(IntPtr client, byte[] name);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_error_code(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_error_message(IntPtr client, byte[] lang);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_retryable(int error);

    /// <summary>UTF-8 with its closing zero, as every clavis_* string argument.</summary>
    public static byte[] Utf8(string s) => Encoding.UTF8.GetBytes(s + "\0");

    /// <summary>A string the library returns: UTF-8, owned by the client until its next call. Read by hand, as
    /// Marshal.PtrToStringUTF8 is missing from .NET Framework and older Unity.</summary>
    public static string Text(IntPtr p)
    {
        if (p == IntPtr.Zero) return "";
        int length = 0;
        while (Marshal.ReadByte(p, length) != 0) length++;
        var bytes = new byte[length];
        Marshal.Copy(p, bytes, 0, length);
        return Encoding.UTF8.GetString(bytes);
    }
}

Open a client for this machine. The API address, App ID and public key are the three values on the Connect software page; in Unity, the state folder should be Application.persistentDataPath. The library keeps the device key and the latest offline token in that folder itself:

IntPtr client = Clavis.clavis_open(
    Clavis.Utf8(Env("CLAVIS_BASE_URL")),
    Clavis.Utf8(Env("CLAVIS_APP_ID")),
    Clavis.Utf8(Env("CLAVIS_APP_KEY")),
    // The device key and the last offline token live here, so sessions and offline use carry over between runs.
    Clavis.Utf8(Env("CLAVIS_STATE_DIR", ".clavis")),
    Clavis.Utf8("1.0.0"));
if (client == IntPtr.Zero)
{
    Console.WriteLine("FAIL settings");
    return 1;
}

Activate the customer's key. Without a network, the offline token saved last time keeps the game running:

int status = Clavis.clavis_init(client);
if (status == 0) status = Clavis.clavis_activate(client, Clavis.Utf8(Env("CLAVIS_LICENSE")));
if (status != 0)
{
    // No answer (network_error, timeout...): the offline token saved last time keeps the software working.
    if (Clavis.clavis_retryable(status) != 0 && Clavis.clavis_offline(client) == 0)
    {
        Console.WriteLine("Offline: using the license saved on this machine");
        Console.WriteLine("OK k1 offline");
        return 0;
    }
    Console.WriteLine(Clavis.Text(Clavis.clavis_error_message(client, lang)));
    Console.WriteLine("FAIL " + Clavis.Text(Clavis.clavis_error_code(client)));
    return 1;
}

The public key only checks signatures, so embedding it in your game is safe. Never embed any secret key.

3. Put it in the game

  • Each call waits for the network, up to a few seconds, so never call on Unity's main thread: the game would freeze. Create a background thread (System.Threading.Thread) that holds the client from opening to closing.
  • Use one client on one thread only. The background thread opens the client, activates, then sends heartbeats on a schedule; the main thread only reads the results it leaves (license state, error sentence), in Update() for example.
  • Strings the library returns stay valid only until the next call on that client; Clavis.Text already copies them into C# strings.
  • Call clavis_close when the game quits (OnApplicationQuit).

4. Run it

Before putting it in Unity, you can run the same code with .NET, without opening Unity:

  1. On the Connect software page, select Make a test key.
  2. Create a console project with dotnet new console -n ClavisCheck, delete Program.cs and put the whole K1.cs below in its place.
  3. Put the library's unpacked folder on the PATH, set the four settings and run dotnet run.
// K1 from C# through the C library: the way Unity, and any .NET that cannot take the C# SDK, calls
// ClavisAUH. Only DllImport and byte arrays, so Mono, IL2CPP and .NET Framework take it as it is (checked against
// netstandard2.0 and C# 9). Put clavis.dll (one file) beside the program or on the PATH, or libclavis.so /
// libclavis.dylib; in Unity, in Assets/Plugins.
// Settings: CLAVIS_BASE_URL, CLAVIS_APP_ID, CLAVIS_APP_KEY, CLAVIS_LICENSE, CLAVIS_STATE_DIR, CLAVIS_LANG.
using System;
using System.Runtime.InteropServices;
using System.Text;

static class Clavis
{
    const string Lib = "clavis";
    const CallingConvention Cdecl = CallingConvention.Cdecl;

    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_open(
        byte[] baseUrl, byte[] appId, byte[] appKey, byte[] stateDir, byte[] appVersion);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern void clavis_close(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_init(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_activate(IntPtr client, byte[] key);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_heartbeat(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_logout(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_offline(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_variable(IntPtr client, byte[] name);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_error_code(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_error_message(IntPtr client, byte[] lang);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_retryable(int error);

    /// <summary>UTF-8 with its closing zero, as every clavis_* string argument.</summary>
    public static byte[] Utf8(string s) => Encoding.UTF8.GetBytes(s + "\0");

    /// <summary>A string the library returns: UTF-8, owned by the client until its next call. Read by hand, as
    /// Marshal.PtrToStringUTF8 is missing from .NET Framework and older Unity.</summary>
    public static string Text(IntPtr p)
    {
        if (p == IntPtr.Zero) return "";
        int length = 0;
        while (Marshal.ReadByte(p, length) != 0) length++;
        var bytes = new byte[length];
        Marshal.Copy(p, bytes, 0, length);
        return Encoding.UTF8.GetString(bytes);
    }
}

static class Program
{
    static string Env(string name, string fallback = "") => Environment.GetEnvironmentVariable(name) ?? fallback;

    static int Main()
    {
        // Messages in Vietnamese need a UTF-8 console.
        Console.OutputEncoding = Encoding.UTF8;
        byte[] lang = Clavis.Utf8(Env("CLAVIS_LANG", "en"));

        IntPtr client = Clavis.clavis_open(
            Clavis.Utf8(Env("CLAVIS_BASE_URL")),
            Clavis.Utf8(Env("CLAVIS_APP_ID")),
            Clavis.Utf8(Env("CLAVIS_APP_KEY")),
            // The device key and the last offline token live here, so sessions and offline use carry over between runs.
            Clavis.Utf8(Env("CLAVIS_STATE_DIR", ".clavis")),
            Clavis.Utf8("1.0.0"));
        if (client == IntPtr.Zero)
        {
            Console.WriteLine("FAIL settings");
            return 1;
        }

        try
        {
            int status = Clavis.clavis_init(client);
            if (status == 0) status = Clavis.clavis_activate(client, Clavis.Utf8(Env("CLAVIS_LICENSE")));
            if (status != 0)
            {
                // No answer (network_error, timeout...): the offline token saved last time keeps the software working.
                if (Clavis.clavis_retryable(status) != 0 && Clavis.clavis_offline(client) == 0)
                {
                    Console.WriteLine("Offline: using the license saved on this machine");
                    Console.WriteLine("OK k1 offline");
                    return 0;
                }
                Console.WriteLine(Clavis.Text(Clavis.clavis_error_message(client, lang)));
                Console.WriteLine("FAIL " + Clavis.Text(Clavis.clavis_error_code(client)));
                return 1;
            }

            // Every few minutes while the software runs; sessions end after an hour without one.
            Clavis.clavis_heartbeat(client);
            string motd = Clavis.Text(Clavis.clavis_variable(client, Clavis.Utf8("motd")));
            Console.WriteLine("Message of the day: " + motd);
            Clavis.clavis_logout(client);
            Console.WriteLine("OK k1 active");
            return 0;
        }
        finally
        {
            Clavis.clavis_close(client);
        }
    }
}
$env:PATH = "C:\path\to\clavis-c-1.0.0-windows-x64;$env:PATH"
$env:CLAVIS_BASE_URL = "<API address>"
$env:CLAVIS_APP_ID = "<App ID>"
$env:CLAVIS_APP_KEY = "<public key>"
$env:CLAVIS_LICENSE = "<test key>"
dotnet run

The last line reads OK k1 active. On Linux, use LD_LIBRARY_PATH pointing at the folder with libclavis.so instead of the PATH.

5. See it on the dashboard

  • Integration › Connect software: step 4 turns to Connected, with c/1.0.0 (the C library) and the machine's operating system.
  • Licensing & releases › Licenses: the key goes from Unused to Active.
  • End users › Devices & sessions: the machine that ran it, and the session it opened.

6. Handle errors

Every int function returns 0 on success, or a fixed error number (the C column of the error codes table). After a failure:

  • clavis_error_code(client): the code as text, such as invalid_credentials or device_limit.
  • clavis_error_message(client, "en"): a sentence for the player, in English or "vi".
  • clavis_retryable(number): not 0 when no answer arrived; trying again later may work. Then clavis_offline(client) returns 0 when the saved offline token is still good on this machine.

While the game runs, send a heartbeat regularly from the background thread, every 5 to 30 minutes or so: a session ends after 60 minutes without one.

// Every few minutes while the software runs; sessions end after an hour without one.
Clavis.clavis_heartbeat(client);
string motd = Clavis.Text(Clavis.clavis_variable(client, Clavis.Utf8("motd")));
Console.WriteLine("Message of the day: " + motd);

Offline tokens come on every plan and last 7 days by default; Premium chooses the length, and Online-Only Mode turns them off. See Offline licenses.

7. Build and release

  • Write the API address, App ID and public key in a script or a ScriptableObject of the game.
  • Unity copies plugins into the build's Plugins folder. Check the build on a clean machine: the game must reach the activated state with nothing else installed (Windows) or with only libsodium and libcurl (Linux).
  • Pass your game's version to clavis_open (its last argument): the server uses it to block builds that are too old and to announce new ones, see Signed releases and updates.
  • Before you sell, turn on Publicly released in Integration › App settings. Every TEST key stops working, so a test key that got out cannot be used.

Questions

Why not the C# SDK? The C# SDK needs .NET 8, which Unity cannot run yet. The C library is the same C++ SDK underneath, with the same protocol and the same signature checks.

Android, iOS, macOS? Not yet. Downloads lists the builds there are.

How do I limit features by license plan? clavis_has(client, "feature_name") tells whether the license grants that feature; add a DllImport line for it like the functions in the Clavis class. Every function is in include/clavis.h in the download.

What if a customer reinstalls the system or changes machines? After a reinstall on the same machine, the server still recognizes it from its hashed hardware identifiers. A new machine takes a new seat; when none is left the customer gets device_limit, and you Reset devices on the Licenses page. See Devices, HWID and transfers.

How can we help?

Frequently asked

What is ClavisAUH?

A licensing and authentication service for desktop software: license keys, device binding, end-user sign-in and signed responses. All run from one dashboard

Why can't a cracker fake your server?

The SDK only trusts answers signed with your app's private key. That key never leaves our servers, so a fake server can't sign and gets rejected

What ends up in my binary?

Your app id and public key. Nothing secret, because each device makes its own key pair on first run

Does it work offline?

Yes. Every plan gets a signed offline token for 7 days, and your app checks it locally. Premium lets you choose how long. A revoked license can keep working offline for that time, unless Online-Only Mode is on

Which languages are supported?

JavaScript and TypeScript today. C++, C# and Python are next, on the same open protocol

Is Basic really free?

Yes. No time limit, no card. Move to Premium when you need more

Get in touch