Linkzly

Unity SDK Setup Guide (C#)

---

14 min read

sidebar_label: Unity (C#) sidebar_category: Gaming SDK Guides

Unity SDK Setup Guide (C#)

This guide covers integrating Gaming Intelligence into a Unity game using C#. There is no prebuilt Unity package — integration is done by adding a single C# wrapper class to your project that uses UnityWebRequest to call the Gaming Intelligence REST API directly. HMAC request signing uses System.Security.Cryptography.HMACSHA256, which is available in every Unity scripting backend including IL2CPP.


Prerequisites

Before you begin, have the following ready:

Requirement Where to Find It
SDK Key Linkzly Console → Gaming → [Your Game] → Settings → SDK Configuration → SDK Key
Organization ID Linkzly Console → Organization Settings
Game ID Linkzly Console → Gaming → [Your Game] → Settings → General Settings
Unity Version Unity 2020.3 LTS or later
Scripting Backend Mono or IL2CPP (both supported)

Your SDK key is masked by default in the console. Use the eye icon to reveal it or the copy button to copy it directly.


Installation

There is no package to install from the Unity Package Manager or Asset Store. Add the integration file manually.

Create a folder at Assets/Linkzly/ and place the following two C# files inside it. The files have no external dependencies — they use only Unity's built-in UnityEngine.Networking and the .NET System.Security.Cryptography namespace.


The Client Class

Create Assets/Linkzly/LinkzlyGaming.cs:

// Assets/Linkzly/LinkzlyGaming.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;

namespace Linkzly
{
    public class GamingOptions
    {
        public string ApiKey;
        public string OrganizationId;
        public string GameId;
        public string GameVersion;
        public string BaseUrl          = "https://gaming.linkzly.com";
        public string SigningSecret;          // Required when HMAC Signing is enabled
        public int    MaxBatchSize     = 100;
        public float  FlushIntervalSeconds = 5f;
        public bool   AutoSessionTracking   = true;
        public bool   Debug            = false;
    }

    public class LinkzlyGaming : MonoBehaviour
    {
        private static LinkzlyGaming _instance;

        /// <summary>
        /// The singleton instance. Only accessible after Configure() has been called.
        /// </summary>
        public static LinkzlyGaming Instance
        {
            get
            {
                if (_instance == null)
                    UnityEngine.Debug.LogWarning(
                        "[LinkzlyGaming] Not initialized. Call LinkzlyGaming.Configure() first.");
                return _instance;
            }
        }

        private GamingOptions _options;
        private string _currentPlayerId;
        private string _currentSessionId;
        private readonly List<Dictionary<string, object>> _queue = new();
        private bool _sessionActive;

        // ── Initialization ────────────────────────────────────────────────────

        /// <summary>
        /// Initialize the SDK. Call once before tracking any events.
        /// The recommended location is a Bootstrap MonoBehaviour that runs first.
        /// Subsequent calls after initialization are silently ignored.
        /// </summary>
        public static void Configure(GamingOptions options)
        {
            if (_instance != null)
            {
                UnityEngine.Debug.LogWarning(
                    "[LinkzlyGaming] Already initialized. Configure() ignored.");
                return;
            }

            var go = new GameObject("LinkzlyGaming");
            DontDestroyOnLoad(go);
            _instance = go.AddComponent<LinkzlyGaming>();
            _instance._options = options;
            _instance._currentSessionId = Guid.NewGuid().ToString();

            if (options.AutoSessionTracking)
            {
                _instance.EnqueueInternal("session_start", null);
                _instance._sessionActive = true;
            }

            _instance.StartCoroutine(_instance.FlushLoop());

            if (options.Debug)
                UnityEngine.Debug.Log("[LinkzlyGaming] SDK initialized.");
        }

        // ── Public API ────────────────────────────────────────────────────────

        /// <summary>
        /// Associate all subsequent events with a player identity.
        /// Call after the player logs in or completes authentication.
        /// </summary>
        public void Identify(string playerId)
        {
            _currentPlayerId = playerId;

            if (_options.Debug)
                UnityEngine.Debug.Log($"[LinkzlyGaming] Player identified: {playerId}");
        }

        /// <summary>
        /// Enqueue a game event. Events are sent automatically on the next flush cycle
        /// or immediately when the queue reaches MaxBatchSize.
        /// </summary>
        public void Track(string eventType, Dictionary<string, object> data = null)
        {
            EnqueueInternal(eventType, data);
        }

        /// <summary>
        /// Force-send all queued events immediately. Call before scene transitions
        /// or any operation that may suspend coroutines.
        /// </summary>
        public void Flush()
        {
            if (_queue.Count == 0) return;
            var batch = new List<Dictionary<string, object>>(_queue);
            _queue.Clear();
            StartCoroutine(SendBatch(batch));
        }

        /// <summary>
        /// End the current session, flush all queued events, and clear the player identity.
        /// Call on logout.
        /// </summary>
        public void Reset()
        {
            if (_options.AutoSessionTracking && _sessionActive)
                EnqueueInternal("session_end", null);

            Flush();
            _currentPlayerId = null;
            _currentSessionId = Guid.NewGuid().ToString();
            _sessionActive = false;
        }

        // ── Unity lifecycle hooks ─────────────────────────────────────────────

        private void OnApplicationPause(bool isPaused)
        {
            if (!_options.AutoSessionTracking) return;

            if (isPaused)
            {
                EnqueueInternal("session_end", null);
                Flush();
                _sessionActive = false;
            }
            else
            {
                _currentSessionId = Guid.NewGuid().ToString();
                EnqueueInternal("session_start", null);
                _sessionActive = true;
            }
        }

        private void OnApplicationQuit()
        {
            if (_options.AutoSessionTracking && _sessionActive)
                EnqueueInternal("session_end", null);

            Flush();
            // Note: coroutines may not complete after OnApplicationQuit.
            // For a guaranteed flush, call Reset() explicitly from your quit UI flow.
        }

        // ── Internal helpers ──────────────────────────────────────────────────

        private void EnqueueInternal(string eventType, Dictionary<string, object> data)
        {
            var evt = new Dictionary<string, object>
            {
                ["event_id"]     = Guid.NewGuid().ToString(),
                ["event_type"]   = eventType,
                ["timestamp"]    = DateTime.UtcNow.ToString("o"),
                ["platform"]     = "unity",
                ["player_id"]    = _currentPlayerId ?? "",
                ["session_id"]   = _currentSessionId,
                ["game_version"] = _options.GameVersion ?? "",
                ["data"]         = data ?? new Dictionary<string, object>()
            };

            _queue.Add(evt);

            if (_options.Debug)
                UnityEngine.Debug.Log(
                    $"[LinkzlyGaming] Queued: {eventType} (queue={_queue.Count})");

            if (_queue.Count >= _options.MaxBatchSize)
                Flush();
        }

        private IEnumerator FlushLoop()
        {
            while (true)
            {
                yield return new WaitForSeconds(_options.FlushIntervalSeconds);
                if (_queue.Count > 0)
                    Flush();
            }
        }

        private IEnumerator SendBatch(List<Dictionary<string, object>> events)
        {
            var payload = new Dictionary<string, object>
            {
                ["batch_id"]        = Guid.NewGuid().ToString(),
                ["batch_timestamp"] = DateTime.UtcNow.ToString("o"),
                ["events"]          = events
            };

            string bodyJson  = MiniJson.Serialize(payload);
            byte[] bodyBytes = Encoding.UTF8.GetBytes(bodyJson);

            string url = $"{_options.BaseUrl}/api/v1/gaming/events";
            var req = new UnityWebRequest(url, "POST")
            {
                uploadHandler   = new UploadHandlerRaw(bodyBytes),
                downloadHandler = new DownloadHandlerBuffer()
            };

            req.SetRequestHeader("Content-Type",       "application/json");
            req.SetRequestHeader("Authorization",      $"Bearer {_options.ApiKey}");
            req.SetRequestHeader("X-Organization-ID",  _options.OrganizationId);
            req.SetRequestHeader("X-Game-ID",          _options.GameId);

            if (!string.IsNullOrEmpty(_options.SigningSecret))
            {
                string ts        = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
                string nonce     = Guid.NewGuid().ToString();
                string signature = ComputeHmac(_options.SigningSecret, ts, nonce, bodyJson);

                req.SetRequestHeader("X-Timestamp",      ts);
                req.SetRequestHeader("X-Nonce",          nonce);
                req.SetRequestHeader("X-Signature-256",  signature);
            }

            yield return req.SendWebRequest();

            if (req.result == UnityWebRequest.Result.Success)
            {
                if (_options.Debug)
                    UnityEngine.Debug.Log(
                        $"[LinkzlyGaming] Batch accepted: {req.downloadHandler.text}");
            }
            else
            {
                UnityEngine.Debug.LogWarning(
                    $"[LinkzlyGaming] Batch send failed ({req.responseCode}): " +
                    $"{req.downloadHandler.text}");
            }
        }

        private static string ComputeHmac(
            string secret, string timestamp, string nonce, string body)
        {
            string signingString = $"{timestamp}.{nonce}.{body}";
            using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
            byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signingString));
            return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
        }
    }
}

MiniJson Helper

UnityWebRequest requires serializing Dictionary<string, object> to JSON, but Unity's built-in JsonUtility does not support dictionaries. Add a second file, Assets/Linkzly/MiniJson.cs, that handles this:

// Assets/Linkzly/MiniJson.cs
// Minimal JSON serializer for Dictionary<string, object> and List<object>.
// If your project already has Newtonsoft.Json (com.unity.nuget.newtonsoft-json),
// replace MiniJson.Serialize() calls with JsonConvert.SerializeObject().
using System;
using System.Collections;
using System.Globalization;
using System.Text;

namespace Linkzly
{
    internal static class MiniJson
    {
        public static string Serialize(object obj)
        {
            var sb = new StringBuilder();
            SerializeValue(obj, sb);
            return sb.ToString();
        }

        private static void SerializeValue(object value, StringBuilder sb)
        {
            switch (value)
            {
                case null:
                    sb.Append("null");
                    break;
                case bool b:
                    sb.Append(b ? "true" : "false");
                    break;
                case string s:
                    sb.Append('"');
                    sb.Append(s
                        .Replace("\\", "\\\\")
                        .Replace("\"", "\\\"")
                        .Replace("\n", "\\n")
                        .Replace("\r", "\\r")
                        .Replace("\t", "\\t"));
                    sb.Append('"');
                    break;
                case IDictionary dict:
                    sb.Append('{');
                    bool firstDict = true;
                    foreach (DictionaryEntry entry in dict)
                    {
                        if (!firstDict) sb.Append(',');
                        sb.Append('"');
                        sb.Append(entry.Key);
                        sb.Append("\":");
                        SerializeValue(entry.Value, sb);
                        firstDict = false;
                    }
                    sb.Append('}');
                    break;
                case IList list:
                    sb.Append('[');
                    bool firstList = true;
                    foreach (var item in list)
                    {
                        if (!firstList) sb.Append(',');
                        SerializeValue(item, sb);
                        firstList = false;
                    }
                    sb.Append(']');
                    break;
                default:
                    sb.Append(Convert.ToString(value, CultureInfo.InvariantCulture));
                    break;
            }
        }
    }
}

If your project already has Newtonsoft.Json installed via the Unity Package Manager (com.unity.nuget.newtonsoft-json), you can delete MiniJson.cs and replace the MiniJson.Serialize(payload) call in LinkzlyGaming.cs with Newtonsoft.Json.JsonConvert.SerializeObject(payload).


Initialization

Call LinkzlyGaming.Configure() once before any events are tracked. The recommended pattern is a GameBootstrap MonoBehaviour attached to a persistent GameObject in your first-loaded scene:

using Linkzly;
using UnityEngine;

public class GameBootstrap : MonoBehaviour
{
    private void Awake()
    {
        LinkzlyGaming.Configure(new GamingOptions
        {
            ApiKey               = "YOUR_SDK_KEY",
            OrganizationId       = "YOUR_ORG_ID",
            GameId               = "YOUR_GAME_ID",
            GameVersion          = Application.version,
            SigningSecret        = "YOUR_SIGNING_SECRET", // Required if HMAC is enabled
            MaxBatchSize         = 50,
            FlushIntervalSeconds = 5f,
            AutoSessionTracking  = true,
            Debug                = Debug.isDebugBuild
        });
    }
}

Configure() creates a new GameObject named LinkzlyGaming marked with DontDestroyOnLoad. It persists across scene loads automatically. Calling Configure() more than once after initialization is silently ignored.

Note: Store credentials outside version-controlled files. Use Unity's ScriptableObject-based configuration assets loaded at runtime, Gradle build scripts, or Xcode build settings to inject ApiKey and SigningSecret at build time rather than committing them to source code.


Configuration Options

Parameter Type Default Required Description
ApiKey string Yes Your game's SDK key from the Linkzly console
OrganizationId string Yes Your organization ID
GameId string Yes Your game ID
GameVersion string Yes Your game's version string. Use Application.version to read from Player Settings
BaseUrl string https://gaming.linkzly.com No API base URL. Override for development or custom deployments
SigningSecret string "" Conditionally HMAC signing secret. Required when HMAC Signing is enabled in Game Settings
MaxBatchSize int 100 No Maximum number of events per outgoing batch. Range: 1–1000
FlushIntervalSeconds float 5 No How often the SDK sends queued events, in seconds
AutoSessionTracking bool true No When true, sends session_start and session_end events automatically via OnApplicationPause and OnApplicationQuit
Debug bool false No Enables verbose logging in the Unity console under the [LinkzlyGaming] prefix

Tracking Events

Identifying the Player

Call Identify() after the player logs in. All events queued after this call will carry the player ID:

LinkzlyGaming.Instance.Identify("player-001");

Tracking a Game Event

Use Track() to send any supported event type. Pass an optional dictionary of key-value data:

// Level completed
LinkzlyGaming.Instance.Track("level_complete", new Dictionary<string, object>
{
    { "level_id",         "level-5" },
    { "score",            12400     },
    { "duration_seconds", 182       },
    { "stars",            3         }
});

// In-app purchase
LinkzlyGaming.Instance.Track("purchase", new Dictionary<string, object>
{
    { "item_id",  "gem-pack-500" },
    { "amount",   4.99f          },
    { "currency", "USD"          },
    { "store",    "apple"        }
});

// Achievement unlocked
LinkzlyGaming.Instance.Track("achievement_unlocked", new Dictionary<string, object>
{
    { "achievement_id",   "first-win"     },
    { "achievement_name", "First Victory" },
    { "points",           50              }
});

// Custom event
LinkzlyGaming.Instance.Track("custom", new Dictionary<string, object>
{
    { "custom_event_name", "boss_defeated" },
    { "boss_id",           "dragon-king"  },
    { "difficulty",        "hard"         },
    { "attempts",          3              }
});

For a complete list of supported event types, see Section 17.1 of the SDKs documentation.

Currency Events

// Player earns in-game currency
LinkzlyGaming.Instance.Track("currency_earned", new Dictionary<string, object>
{
    { "amount",        500           },
    { "currency_type", "gold"        },
    { "source",        "level_reward" }
});

// Player spends in-game currency
LinkzlyGaming.Instance.Track("currency_spent", new Dictionary<string, object>
{
    { "amount",        200      },
    { "currency_type", "gold"   },
    { "reason",        "item_buy" }
});

Session Management

Automatic Session Tracking (Default)

When AutoSessionTracking = true, the SDK manages sessions for you using Unity's OnApplicationPause and OnApplicationQuit callbacks:

  • A session_start event is sent when Configure() is called.
  • A session_end event is sent and queued events are flushed when the app is paused (goes to background).
  • A new session_id UUID is generated and a new session_start event is sent when the app resumes from pause (returns to foreground).

You do not need to call any session methods manually when using automatic tracking.

Manual Session Tracking

To control sessions yourself, set AutoSessionTracking = false and call Track() with session event types explicitly:

// On gameplay start (e.g., after the player taps "Play")
LinkzlyGaming.Instance.Track("session_start");

// On gameplay end or intentional exit
LinkzlyGaming.Instance.Track("session_end");
LinkzlyGaming.Instance.Flush();

Flushing Before Exit

Call Reset() from your quit button handler to send a session_end event and flush before Application.Quit():

public void OnQuitButtonPressed()
{
    LinkzlyGaming.Instance.Reset();
    Application.Quit();
}

Note on OnApplicationQuit: Unity does not guarantee coroutines complete before the process exits. For the final flush, call Reset() from your own quit UI flow rather than relying solely on OnApplicationQuit. On iOS and Android, OnApplicationPause(true) fires reliably when the OS suspends your app and is the primary flush trigger for those platforms.


HMAC Request Signing

HMAC signing is enabled by default for all new games. When enabled, every request must include a valid signature computed from your signing secret.

Pass SigningSecret in GamingOptions — the SDK computes the signature and attaches X-Signature-256, X-Timestamp, and X-Nonce to every outgoing batch automatically:

LinkzlyGaming.Configure(new GamingOptions
{
    ApiKey         = "YOUR_SDK_KEY",
    OrganizationId = "YOUR_ORG_ID",
    GameId         = "YOUR_GAME_ID",
    GameVersion    = Application.version,
    SigningSecret  = "YOUR_SIGNING_SECRET"
});

How the signature is computed:

  1. Build the signing string: "{timestamp}.{nonce}.{rawBodyString}"
  2. Compute HMAC-SHA256 of the signing string using your signing secret as the key (System.Security.Cryptography.HMACSHA256).
  3. Hex-encode the resulting bytes to lowercase.
  4. Send the result in X-Signature-256, along with X-Timestamp (Unix milliseconds) and X-Nonce (UUID v4).

The replay window is fixed at 300 seconds. Requests timestamped more than 5 minutes out of sync with the server are rejected with 401 Unauthorized. The SDK uses DateTimeOffset.UtcNow for timestamps — ensure the device clock is synchronized.

Your signing secret is available in the console under Game SettingsSDK Configuration. It is separate from the SDK Key.

Security note: Do not hardcode your signing secret in source files committed to version control. Use a ScriptableObject configuration asset loaded at runtime, Unity Cloud Build environment variables, or Xcode/Gradle build settings to supply the value at build time without embedding it in source code.

To disable HMAC for local development, toggle off HMAC Signing Required in Game SettingsSecurity Settings and omit SigningSecret from GamingOptions. Re-enable it before releasing to production.


Batch Limits

The ingestion endpoint enforces the following limits per request:

Limit Value
Maximum events per batch 1,000
Maximum body size 1 MB
Maximum size per event 10 KB
Maximum event age 24 hours

The MaxBatchSize option (default: 100) is a client-side limit that triggers an early flush when the queue reaches that size. Setting it lower reduces memory usage in games with high event volumes. Do not set it above 1,000.

Events that violate the per-event or timestamp limits are dropped by the server and counted in the events_dropped field of the batch response. Enable Debug = true to log dropped event details to the Unity console.


IL2CPP and WebGL Compatibility

System.Security.Cryptography.HMACSHA256 is supported on all platforms under IL2CPP. If you see code stripping errors at runtime, add the following to Assets/link.xml:

<linker>
  <assembly fullname="System.Core">
    <type fullname="System.Security.Cryptography.HMACSHA256" preserve="all" />
  </assembly>
</linker>

For WebGL builds, HMACSHA256 is available when using the IL2CPP backend with .NET Standard 2.1 or higher. UnityWebRequest works in WebGL — requests are made via XMLHttpRequest under the hood. The Linkzly ingestion endpoint permits cross-origin requests.


Troubleshooting

Events are not appearing in the Linkzly dashboard

  1. Set Debug = true in GamingOptions and check the Unity console for [LinkzlyGaming] entries.
  2. Verify ApiKey, OrganizationId, and GameId are correct (no trailing spaces or newlines).
  3. Confirm LinkzlyGaming.Configure() is called before any Track() calls.
  4. On Android, confirm android.permission.INTERNET is present in AndroidManifest.xml. On iOS, confirm the app is not blocked by App Transport Security for gaming.linkzly.com.

HMAC errors (401 Unauthorized)

  1. Confirm SigningSecret in GamingOptions matches the secret shown in Game Settings → SDK Configuration exactly.
  2. Verify the device clock is synchronized. The replay window is fixed at 300 seconds — requests with timestamps more than 5 minutes out of sync are rejected.
  3. Check the Unity console for [LinkzlyGaming] Batch send failed (401) entries and inspect the response body for a specific error message.

High events_dropped count in batch responses

Events are dropped if they exceed the 10 KB per-event limit, have a timestamp older than 24 hours, or carry a future timestamp more than 5 minutes ahead. Enable Debug = true to log the server response for each batch, which includes drop reasons.

NullReferenceException when calling LinkzlyGaming.Instance

Instance is null if Configure() has not been called. Ensure your GameBootstrap script runs before any script that calls Track(). Use Unity's Edit → Project Settings → Script Execution Order to explicitly set GameBootstrap to a lower order number (earlier execution).

Events lost on application quit

Unity does not guarantee coroutines complete after OnApplicationQuit. Call Reset() explicitly from your quit button handler before Application.Quit() to ensure the final session_end event and any queued events are flushed.

Was this helpful?

Help us improve our documentation