> ## Documentation Index
> Fetch the complete documentation index at: https://docs.immutable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Unity SDK

> Typed tracking SDK for Unity games on PC and mobile. Captures session lifecycle, progressions, purchases, and identity signals

> [!WARNING]
>
> The Unity SDK is currently in **alpha**. APIs and behavior may change between releases.

> [!NOTE]
>
> **Who is this for?** Unity engineers building games on **Windows (64-bit), macOS, Linux (64-bit), iOS, or Android** who want typed in-game tracking, identity, and offline-safe queuing.

The Immutable Unity SDK instruments the in-game layer of the player journey for Audience analytics and [Attribution](https://docs.immutable.com/docs/products/attribution/overview.md). It connects campaigns and referral links to in-game behavior. Session lifecycle and a launch event are captured automatically. In-game moments like progressions, purchases, and milestones are triggered by your code.

## What You Need

* An [Immutable Hub](https://hub.immutable.com) account with a project
* A publishable key from your project settings ([API keys guide](https://docs.immutable.com/docs/guides/advanced-setup/api-keys.md))

- A target build platform. Tested on:
  * **Windows 10+ (64-bit)**
  * **macOS Sequoia 15.7.4+**
  * **Linux Ubuntu 22.04 LTS+ (64-bit)**
  * **iOS 13+**
  * **Android 5.0+ (API level 21+)**
- A Unity project on a supported version:
  * [Unity 2021.3.45f2+](unityhub://2021.3.45f2/88f88f591b2e)
  * [Unity 2022.3.62f2+](unityhub://2022.3.62f2/7670c08855a9)
  * [Unity 6000.4.0f1+](unityhub://6000.4.0f1/8cf496087c8f)
- A Unity project using **Mono** or **IL2CPP** as the scripting backend (both are fully supported)

Mobile builds targeting iOS or Android with attribution signals require additional setup. See [Mobile](#mobile).

## Installation

Install the package via Unity Package Manager using a Git URL.

1. In Unity, open **Window → Package Manager**.
2. Click the **+** button in the top-left, then **Add package from git URL...**.
3. Paste the URL below and click **Add**:

```
https://github.com/immutable/unity-immutable-sdk.git?path=src/Packages/Audience#audience/v0.7.3
```

The package appears in your project as **Immutable Audience** under **Packages** in the Project window.

To update to a newer release, edit the version tag in `Packages/manifest.json` directly.

## Quick Start

> [!NOTE]
>
> Prefer learning from a running project? A working Unity sample lives at [unity-immutable-sdk/examples/audience](https://github.com/immutable/unity-immutable-sdk/tree/main/examples/audience). Clone the repository, set your publishable key, and run.

**1. Initialize the SDK at boot**

Call `ImmutableAudience.Init` once at game launch. You can mark a static method with `[RuntimeInitializeOnLoadMethod]` to have Unity call it before any scene loads.

```csharp theme={null}
using Immutable.Audience;
using UnityEngine;

public static class Analytics
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void Init()
    {
        ImmutableAudience.Init(new AudienceConfig
        {
            PublishableKey = "YOUR_PUBLISHABLE_KEY",
            Consent = ConsentLevel.Anonymous,
            DistributionPlatform = DistributionPlatforms.Steam,
            Debug = true,
        });
    }
}
```

> [!WARNING]
>
> Consent defaults to `ConsentLevel.None`, which suppresses all tracking. The example above sets `Anonymous` directly so events flow immediately. For an in-game privacy prompt, see the next step.

**2. Set consent**

The SDK uses a three-tier consent model (`None`, `Anonymous`, `Full`). The default is `None`. The SDK does not collect anything until you explicitly raise it. The SDK does not provide a consent UI. Build your own in-game prompt and call `SetConsent` when the player responds.

```csharp theme={null}
// Player accepts anonymous tracking
ImmutableAudience.SetConsent(ConsentLevel.Anonymous);

// Player logs in and accepts full tracking
ImmutableAudience.SetConsent(ConsentLevel.Full);

// Player revokes consent (clears identity, purges queued events, stops collection)
ImmutableAudience.SetConsent(ConsentLevel.None);
```

`SetConsent` takes effect immediately. Lowering the level purges queued events the new level no longer permits. See the [Data Dictionary](https://docs.immutable.com/docs/products/audience/analytics/data-dictionary.md#consent-model) for what each level collects.

**3. Track events**

Use `ImmutableAudience.Track` to log a player action. The SDK ships with [predefined events](https://docs.immutable.com/docs/products/audience/analytics/data-dictionary.md#predefined-events) for common player actions and accepts any custom event string.

```csharp theme={null}
// Predefined event with typed properties
ImmutableAudience.Track(new Purchase
{
    Currency = "USD",
    Value = 9.99m,
    ItemName = "Sword of Truth",
});

// Custom event with any properties
ImmutableAudience.Track("tutorial_complete", new Dictionary<string, object>
{
    ["stepCount"] = 5,
});
```

**4. Identify users**

Identifies the player by associating their userId with their in-game activity and traits. Call at login or account connection. Requires `Full` consent.

```csharp theme={null}
ImmutableAudience.Identify(
    userId: "76561190000000000",
    identityType: IdentityType.Steam,
    traits: new Dictionary<string, object>
    {
        ["personaName"] = "Player1",
    });
```

**5. Clean up on exit**

Called automatically on `Application.quitting`. For manual teardown:

```csharp theme={null}
// Manual teardown: flush pending events, then stop the SDK.
await ImmutableAudience.FlushAsync();
ImmutableAudience.Shutdown();
```

### Complete Example

```csharp theme={null}
using System.Collections.Generic;
using Immutable.Audience;
using UnityEngine;

public static class Analytics
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static async void Init()
    {
        // 1. Initialize
        ImmutableAudience.Init(new AudienceConfig
        {
            PublishableKey = "YOUR_PUBLISHABLE_KEY",
        });

        // 2. Set consent after privacy prompt
        ImmutableAudience.SetConsent(ConsentLevel.Anonymous);

        // 3. Track player actions
        ImmutableAudience.Track(new Purchase { Currency = "USD", Value = 9.99m });

        // 4. Identify after login (requires Full consent)
        ImmutableAudience.SetConsent(ConsentLevel.Full);
        ImmutableAudience.Identify("76561190000000000", IdentityType.Steam, new Dictionary<string, object>
        {
            ["personaName"] = "Player1",
        });

        // 5. Clean up on exit (auto-called on Application.quitting)
        await ImmutableAudience.FlushAsync();
        ImmutableAudience.Shutdown();
    }
}
```

## Auto-Tracked Events

The SDK fires `game_launch`, `session_start`, `session_end`, and `session_heartbeat` automatically, without any `Track` calls. On mobile with attribution enabled (see [Mobile](#mobile)), it also fires `tracking_authorization_changed` and `install_referrer_received`.

For the full property set on each, see [Auto-Tracked Events](https://docs.immutable.com/docs/products/audience/analytics/data-dictionary.md#auto-tracked-events) in the Data Dictionary.

## Verify the Integration

After Init, the SDK exposes its state through read-only properties on `ImmutableAudience`. Log them to confirm a healthy install:

```csharp theme={null}
Debug.Log($"Initialized: {ImmutableAudience.Initialized}");
Debug.Log($"AnonymousId: {ImmutableAudience.AnonymousId}");
Debug.Log($"SessionId: {ImmutableAudience.SessionId}");
Debug.Log($"QueueSize: {ImmutableAudience.QueueSize}");
```

Pair with an `OnError` callback in your `AudienceConfig` to surface failures (see [Error Handling](#error-handling)):

```csharp theme={null}
OnError = err => Debug.LogWarning($"{err.Code}: {err.Message}"),
```

A healthy install:

| Property | Healthy value |
| - | - |
| `Initialized` | `true` |
| `AnonymousId` | non-null GUID |
| `SessionId` | non-null GUID once [`session_start`](https://docs.immutable.com/docs/products/audience/analytics/data-dictionary#auto-tracked-events) has fired |
| `QueueSize` | `1` immediately after a `Track` call, then `0` once flushed |
| Unity Console | no SDK warnings, no `OnError` invocations |

## Mobile

Mobile adds opt-in device-level attribution signals for iOS and Android.

### Attribution opt-in

Two gates must both be set before any mobile attribution data ships:

| Gate | Where |
| - | - |
| **Build-time** `AUDIENCE_MOBILE_ATTRIBUTION` | Tick **Enable for iOS** / **Enable for Android** on the Mobile Build Settings asset (see [Platform setup](#platform-setup)). Set separately per platform. |
| **Runtime** `EnableMobileAttribution = true` | [`AudienceConfig` passed to `Init`](#initaudienceconfig). Controls whether attribution data is collected at runtime. |

Setting `AUDIENCE_MOBILE_ATTRIBUTION` does three things: adds the `AD_ID` manifest permission on Android, compiles in native attribution code, and switches the iOS privacy manifest to the tracking variant (`NSPrivacyTracking = true`).

If neither gate is set, the build ships without attribution code: no `AD_ID` manifest permission, no native tracking, and `NSPrivacyTracking = false` in the iOS privacy manifest.

### Platform setup

> [tab] iOS
>
> **1. Create a Mobile Build Settings asset**
>
> The iOS build post-processors read three things off this asset: whether attribution is enabled for iOS, the `NSUserTrackingUsageDescription` string (required, Apple rejects builds that omit it), and the SKAdNetwork IDs you want install attribution from (optional, but empty means no SKAdNetwork data ships). Create the asset once per Unity project:
>
> 1. **Assets → Create → Immutable Audience → Mobile Build Settings**
> 2. Tick **Enable for iOS** at the top of the asset. This writes the `AUDIENCE_MOBILE_ATTRIBUTION` define for you.
> 3. Set **Tracking Usage Description** to a specific description of what you collect and why. Apple rejects generic copy.
> 4. Add each ad network you run install campaigns through to **SKAdNetwork IDs**. Get each ID from that network's own SDK docs, or from the [IAB Tech Lab list](https://iabtechlab.com/software/skadnetwork-id-list/).
>
> **2. Request [App Tracking Transparency (ATT)](https://developer.apple.com/documentation/apptrackingtransparency) authorization**
>
> Call `RequestTrackingAuthorizationAsync` at a contextually appropriate moment. The IDFA, when authorized, ships automatically in the next [`game_launch`](https://docs.immutable.com/docs/products/audience/analytics/data-dictionary#auto-tracked-events). A `tracking_authorization_changed` event fires on any subsequent status transition.
>
> ATT requires iOS 14+. On iOS 13 the call resolves to `NotDetermined` and IDFA is unavailable. All other SDK features work normally.
>
> ```csharp theme={null}
> var status = await ImmutableAudience.RequestTrackingAuthorizationAsync();
> ```
>
> **[Privacy manifest](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files) (automatic)**
>
> No action required. The SDK ships a `PrivacyInfo.xcprivacy` that Unity merges into the generated Xcode project. The post-processor selects the correct variant based on the `AUDIENCE_MOBILE_ATTRIBUTION` define:
>
> | Build | `NSPrivacyTracking` | Declared data types |
> | - | - | - |
> | Default | `false` | IDFV (`NSPrivacyCollectedDataTypeDeviceID`) |
> | `AUDIENCE_MOBILE_ATTRIBUTION` | `true` | IDFV + IDFA (`NSPrivacyCollectedDataTypeAdvertisingData`) |
>
> No Required Reason APIs are used by the SDK. `NSPrivacyAccessedAPITypes` in the manifest reflects Unity engine internals only.

> [tab] Android
>
> Tick **Enable for Android** on the Mobile Build Settings asset first (see the iOS tab for where to create it, it's the same asset for both platforms).
>
> Android attribution uses two Google libraries when `AUDIENCE_MOBILE_ATTRIBUTION` is set. The SDK resolves them from Maven rather than bundling them as `.aar` files, because a bundled AAR can't be de-duplicated by Gradle and conflicts with any other SDK that pulls the same library.
>
> | Dependency | Powers |
> | - | - |
> | `com.android.installreferrer:installreferrer:2.2` | [Play Install Referrer](https://developer.android.com/google/play/installreferrer): the [`install_referrer_received`](https://docs.immutable.com/docs/products/audience/analytics/data-dictionary#auto-tracked-events) event, fired once per install |
> | `com.google.android.gms:play-services-ads-identifier:18.0.1` | Google Advertising ID (`gaid`, `gaid_limit_ad_tracking`) |
>
> **Using EDM4U (recommended)**
>
> With the [External Dependency Manager for Unity](https://github.com/googlesamples/unity-jar-resolver) in your project, the Install Referrer dependency resolves automatically, since the SDK ships its own `ImmutableAudienceDependencies.xml`. Add only the GAID dependency to a `*Dependencies.xml` anywhere under an `Editor/` folder in your project:
>
> ```xml theme={null}
> <dependencies>
>   <androidPackages>
>     <androidPackage spec="com.google.android.gms:play-services-ads-identifier:18.0.1"/>
>   </androidPackages>
> </dependencies>
> ```
>
> GAID is left to you (rather than declared by the SDK) so builds that don't collect it never request the `AD_ID` permission.
>
> **Without EDM4U**
>
> Enable **Custom Main Gradle Template** under **Player Settings → Publishing Settings**, then add both dependencies to `Assets/Plugins/Android/mainTemplate.gradle`. The `**DEPS**` token is a Unity placeholder expanded at build time, so do not remove it:
>
> ```gradle theme={null}
> dependencies {
>     implementation fileTree(dir: 'libs', include: ['*.jar'])
> **DEPS**
>     implementation 'com.android.installreferrer:installreferrer:2.2'
>     implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1'
> }
> ```
>
> Without the GAID dependency, a `ClassNotFoundException` is logged on each launch when `AUDIENCE_MOBILE_ATTRIBUTION` is set and `gaid` never ships. The SDK fetches GAID on a background thread (required by Google).
>
> > [!NOTE]
> >
> > GAID is unavailable on first launch. The first [`game_launch`](https://docs.immutable.com/docs/products/audience/analytics/data-dictionary#auto-tracked-events) event will not include `gaid` or `gaid_limit_ad_tracking`. Both are available from the second launch onwards.

## API Reference

All methods are static on `Immutable.Audience.ImmutableAudience` and are safe to call from any thread after `Init` returns.

> [accordion] Init(AudienceConfig)
>
> Starts the SDK. Call once at game launch (see the [Quick Start](#quick-start)). Subsequent calls are ignored with a warning.
>
> Throws `ArgumentNullException` if `config` is null, and `ArgumentException` if `PublishableKey` is empty.
>
> ```csharp theme={null}
> ImmutableAudience.Init(new AudienceConfig
> {
>     PublishableKey = "YOUR_PUBLISHABLE_KEY",
>     Consent = ConsentLevel.None,
>     DistributionPlatform = DistributionPlatforms.Steam,
>     Debug = true,
>     OnError = err => Debug.LogWarning($"{err.Code}: {err.Message}"),
> });
> ```
>
> **`AudienceConfig` fields:**
>
> | Field | Type | Required | Default | Description |
> | - | - | - | - | - |
> | `PublishableKey` | `string` | Yes | (none) | API key from [Hub](https://hub.immutable.com). |
> | `Consent` | `ConsentLevel` | No | `None` | Initial consent level. The SDK collects nothing until this is `Anonymous` or higher. |
> | `DistributionPlatform` | `string?` | No | null | The platform the build is shipping on. `Init` normalizes this to lowercase, so case at the call site does not matter. Use `DistributionPlatforms.*` constants for the standard values ([see below](#distribution-platforms)). |
> | `Debug` | `bool` | No | `false` | Enable SDK warnings via `UnityEngine.Debug.Log`. Disable in release builds. |
> | `TestMode` | `bool` | No | `false` | Mark all events sent in this session as test traffic. Use during development or QA to separate test events from production data in analytics. |
> | `FlushIntervalSeconds` | `int` | No | `5` | How often pending events are flushed to the backend. |
> | `FlushSize` | `int` | No | `20` | Flush as soon as this many events are queued. |
> | `OnError` | `Action<AudienceError>?` | No | null | Callback fired on errors. Runs on a background thread. Marshal to the main thread before touching Unity APIs. `AudienceError.Code` is one of `FlushFailed`, `ValidationRejected`, `ConsentSyncFailed`, `NetworkError`, `ConsentPersistFailed` (see [Error Handling](#error-handling) for descriptions). |
> | `PersistentDataPath` | `string?` | No | (auto from Unity) | Directory for the SDK identity, consent, and queued-event files. The Unity integration fills this in from `Application.persistentDataPath`. |
> | `ShutdownFlushTimeoutMs` | `int` | No | `2000` | Maximum time `Shutdown` waits for the final flush. |
> | `EnableMobileAttribution` | `bool` | No | `false` | Opts into mobile attribution signals ([ATT](https://developer.apple.com/documentation/apptrackingtransparency)/IDFA/[SKAdNetwork](https://developer.apple.com/documentation/storekit/skadnetwork) on iOS, GAID/Install Referrer on Android). Both this flag and the `AUDIENCE_MOBILE_ATTRIBUTION` scripting define must be set. See [Mobile](#mobile). |
>
> #### Distribution platforms
>
> `DistributionPlatform` is a free-form string sent in the `distribution_platform` property of the [`game_launch`](https://docs.immutable.com/docs/products/audience/analytics/data-dictionary#auto-tracked-events) event. `Init` lowercases the value before it goes on the wire. Use the `DistributionPlatforms` constants when one matches:
>
> | Constant | Value |
> | - | - |
> | `DistributionPlatforms.Steam` | `"steam"` |
> | `DistributionPlatforms.Epic` | `"epic"` |
> | `DistributionPlatforms.GOG` | `"gog"` |
> | `DistributionPlatforms.Itch` | `"itch"` |
> | `DistributionPlatforms.Standalone` | `"standalone"` |
>
> If you ship on a platform not in this list, pass the lowercase short name as a string.
>
> If you leave `DistributionPlatform` unset and the SDK detects Steam or Epic running at `Init` (see "Auto-collection on Steam and Epic" under `Identify` below), it fills this in for you. Setting `DistributionPlatform` explicitly always wins over auto-detection.

> [accordion] SetConsent(ConsentLevel level)
>
> Changes the consent level. Pass `ConsentLevel.None`, `ConsentLevel.Anonymous`, or `ConsentLevel.Full`. Takes effect immediately. Lowering the level purges queued events that the new level no longer permits. Lowering to `None` also clears the anonymous ID.
>
> ```csharp theme={null}
> ImmutableAudience.SetConsent(ConsentLevel.Full);
> ```
>
> The new level is persisted to disk so it survives the next launch. The SDK also notifies the backend asynchronously for audit logging. Failures fire `OnError` with `AudienceErrorCode.ConsentSyncFailed` but do not affect local state.

> [accordion] Track(IEvent)
>
> Sends a predefined event. See the [Data Dictionary](https://docs.immutable.com/docs/products/audience/analytics/data-dictionary.md#predefined-events) for available event classes and their schemas.
>
> | Parameter | Type | Description |
> | - | - | - |
> | `evt` | `IEvent` | The typed event instance. Null events are dropped with a log warning. |
>
> **Requires:** `Anonymous` or `Full` consent. Calls at `None` are silently dropped.
>
> ```csharp theme={null}
> ImmutableAudience.Track(new Purchase { Currency = "USD", Value = 9.99m });
> ```

> [accordion] Track(string eventName, Dictionary\<string, object>? properties)
>
> Sends a custom event with arbitrary properties. Prefer [`Track(IEvent)`](#trackievent) for predefined events instead of building the dictionary by hand. Property values can be any JSON-serializable primitive or string. Strings are truncated at 256 characters.
>
> Throws `ArgumentException` if `eventName` is one of the reserved names (`purchase`, `progression`, `resource`, `achievement_unlocked`, `milestone_reached`) and a required field for that event is missing, even though this is the string overload rather than the typed one.
>
> | Parameter | Type | Description |
> | - | - | - |
> | `eventName` | `string` | Required, non-empty. The custom event name. Null or empty values are dropped with a log warning. |
> | `properties` | `Dictionary<string, object>?` | Optional. Event properties. Defaults to `null`. |
>
> **Requires:** `Anonymous` or `Full` consent.
>
> ```csharp theme={null}
> ImmutableAudience.Track("tutorial_complete", new() { ["stepCount"] = 5 });
> ```

> [accordion] Identify(string userId, IdentityType identityType, Dictionary? traits = null)
>
> Associates the player's userId with their in-game activity and traits. Call at login or account connection. Use `IdentityType.Custom` for providers not in the enum.
>
> | Parameter | Type | Description |
> | - | - | - |
> | `userId` | `string` | Required, non-empty. The player's identifier in the chosen provider. The SDK silently drops calls with null or empty `userId` (a `Debug.LogWarning` is emitted) so a missing ID does not crash the game. Gate calls on a populated value during development. |
> | `identityType` | `IdentityType` | Required. Which provider the ID comes from (see table below). |
> | `traits` | `Dictionary<string, object>?` | Optional. Custom user traits (e.g. `personaName`). Defaults to `null`. |
>
> **Requires:** `Full` consent. Calls at lower levels are dropped with a log warning.
>
> ```csharp theme={null}
> ImmutableAudience.Identify("76561190000000000", IdentityType.Steam, new() { ["personaName"] = "Player1" });
> ```
>
> **Identity types:**
>
> | Value | Provider |
> | - | - |
> | `IdentityType.Passport` | Passport |
> | `IdentityType.Steam` | Steam |
> | `IdentityType.Epic` | Epic Games |
> | `IdentityType.Google` | Google |
> | `IdentityType.Apple` | Apple |
> | `IdentityType.Discord` | Discord |
> | `IdentityType.Email` | Email address |
> | `IdentityType.Custom` | Custom identity system |
>
> **Auto-collection on Steam and Epic**
>
> When `DistributionPlatform` is `"steam"` or `"epic"` and consent is `Full` at `Init`, the SDK calls `Identify` for you. The platform session must already be active when `Init` runs:
>
> * **Steam**: [Steamworks.NET](https://steamworks.github.io) or [Facepunch.Steamworks](https://wiki.facepunch.com/steamworks) present, with Steam running. If you use Facepunch, call `SteamClient.Init(appId)` before `ImmutableAudience.Init`.
> * **Epic**: the [PlayEveryWare EOS Unity plugin](https://github.com/PlayEveryWare/eos_plugin_for_unity) installed, with EOS initialized and the player logged in.
>
> Auto-collection runs once at `Init`. If your consent flow starts below `Full`, call `Identify` manually when the player grants full consent. If your game also has its own backend user ID, link it with `Alias` rather than calling `Identify` again.

> [accordion] Alias(string fromId, IdentityType fromType, string toId, IdentityType toType)
>
> Links two user IDs that belong to the same player. Call when a player connects a second account (e.g. a player with an internal game account ID later signs in via Steam).
>
> | Parameter | Type | Description |
> | - | - | - |
> | `fromId` | `string` | Required, non-empty. The account being linked. |
> | `fromType` | `IdentityType` | Required. The provider for `fromId`. |
> | `toId` | `string` | Required, non-empty. The player's main account. |
> | `toType` | `IdentityType` | Required. The provider for `toId`. |
>
> **Requires:** `Full` consent. Both `fromType` and `toType` are required for data-deletion matching.
>
> ```csharp theme={null}
> ImmutableAudience.Alias("76561190000000000", IdentityType.Steam, "internal-player-id-12345", IdentityType.Custom);
> ```

> [accordion] Reset()
>
> Wipes the current player's identity, generates a fresh anonymous ID, and **discards queued events** (memory and disk) so the next player on the device isn't attributed to the previous one. Call when a player logs out. Reset does not change the consent level, so studios using an explicit consent prompt should also call `SetConsent(ConsentLevel.None)` at logout if the next player should start unconsented.
>
> To send queued events before they're discarded:
>
> ```csharp theme={null}
> await ImmutableAudience.FlushAsync();
> ImmutableAudience.Reset();
> ```

> [accordion] FlushAsync()
>
> Sends all queued events to the server immediately. Returns a `Task` that completes when the queue is empty or a backoff window prevents further sends. Call before `Reset` (which discards queued events) or `Shutdown` (which is capped at `ShutdownFlushTimeoutMs`) if you want every pending event delivered first. When calling `FlushAsync` in a logout flow, pass a `CancellationToken` with a deadline so the call does not hang if the network is unavailable at logout time, for example `new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token`.
>
> ```csharp theme={null}
> await ImmutableAudience.FlushAsync();
> ```

> [accordion] Shutdown()
>
> Flushes any pending events (capped at `ShutdownFlushTimeoutMs`, default 2 seconds) and stops the SDK. Called automatically on `Application.quitting`. You do not need to call it yourself unless you want to control the flush timing or reinitialize with new config.
>
> ```csharp theme={null}
> ImmutableAudience.Shutdown();
> ```

> [accordion] RequestTrackingAuthorizationAsync() (iOS only)
>
> Shows the iOS [App Tracking Transparency](https://developer.apple.com/documentation/apptrackingtransparency) prompt and returns the user's decision. The prompt appears once per app install; subsequent calls return the cached status without showing the prompt again.
>
> On non-iOS platforms, iOS 13, or before the SDK is initialized, resolves to `NotDetermined`.
>
> **Requires:** `AUDIENCE_MOBILE_ATTRIBUTION` define + `EnableMobileAttribution = true`.
>
> ```csharp theme={null}
> var status = await ImmutableAudience.RequestTrackingAuthorizationAsync();
> ```
>
> | Value | Meaning |
> | - | - |
> | `NotDetermined` | Not yet prompted, dismissed, or non-iOS platform |
> | `Restricted` | Restricted by device policy or parental controls. System prompt is not shown. |
> | `Denied` | User denied tracking. IDFA is unavailable. |
> | `Authorized` | User authorized tracking. IDFA is included in the next `game_launch`. |
>
> Call at a moment when the value exchange is clear to the player. Apple's HIG forbids prompting on cold launch. After the user responds, the SDK fires `tracking_authorization_changed` if the status differs from the previously stored value.

> [accordion] Diagnostic getters
>
> Read-only properties for inspecting SDK state. Use these to verify the SDK is initialized, gate UI elements that depend on consent state (the Unity SDK does not currently ship dedicated `CanTrack` or `CanIdentify` helpers, so read `CurrentConsent` and compare against `ConsentLevel.Anonymous` or `ConsentLevel.Full` instead), or watch the queue size during development.
>
> | Property | Type | Description |
> | - | - | - |
> | `Initialized` | `bool` | True between `Init()` and `Shutdown()` |
> | `CurrentConsent` | `ConsentLevel` | The current consent level |
> | `UserId` | `string?` | Set by the most recent `Identify` call. Null before `Identify`, after `Reset`, or when consent is below `Full`. |
> | `CurrentIdentityType` | `IdentityType?` | The provider from the most recent `Identify` call. Null before `Identify`, after `Reset`, or when consent is below `Full`. |
> | `AnonymousId` | `string?` | Persistent ID separate from `UserId` and `SessionId`. Null while consent is `None`. |
> | `DeviceId` | `string?` | Persistent per-device ID that, unlike `AnonymousId`, survives `Reset`. Lets the backend resolve a returning player without a fresh `Identify` call. Null while consent is `None`. |
> | `SessionId` | `string?` | The current session's GUID. Null while consent is `None`. |
> | `QueueSize` | `int` | Number of unsent events (memory + disk) |
>
> ```csharp theme={null}
> // Gate a tracking-dependent UI element on consent
> if (ImmutableAudience.CurrentConsent >= ConsentLevel.Anonymous)
> {
>     ShowAnalyticsBadge();
> }
>
> // Force-flush the queue when it grows large
> if (ImmutableAudience.Initialized && ImmutableAudience.QueueSize > 100)
> {
>     await ImmutableAudience.FlushAsync();
> }
> ```

## Event Delivery

* Events are batched and flushed every **5 seconds** or when **20 events** accumulate. Configure via the `FlushIntervalSeconds` and `FlushSize` fields on `AudienceConfig` (see [`Init`](#api-reference)).
* `Application.quitting` triggers a final flush capped at `ShutdownFlushTimeoutMs` (default 2 seconds).
* Events not flushed before quit are persisted to disk and shipped on the next launch. Events older than 30 days are discarded.

## Error Handling

Flushes run on a background thread with automatic retries. Most failures don't surface to your code. Pass an `OnError` callback when configuring [`Init`](#api-reference) to observe them.

`AudienceError.Code` is one of:

| Code | Description |
| - | - |
| `FlushFailed` | An event batch failed to flush. Either a local storage read error (batch dropped) or a non-2xx, non-4xx server response (typically 5xx). On 5xx, the batch is retained and retried with exponential `backoff`. |
| `ValidationRejected` | The server rejected an event batch with a 4xx status. The batch was dropped. Retrying will not help (typically a malformed payload). |
| `ConsentSyncFailed` | Failed to sync a consent change to the backend. The local consent level has already been applied. The server-side audit log may be out of date. |
| `NetworkError` | A network call failed (exception, timeout, or non-2xx server response). Event batches are retained for retry. |
| `ConsentPersistFailed` | Failed to persist the consent level to disk. The in-memory level still applies but reverts on next launch. |

When `Code` is `ValidationRejected`, `AudienceError.Rejections` tells you exactly which field failed and why, one entry per message in the rejected batch:

```csharp theme={null}
OnError = err =>
{
    if (err.Rejections == null) return;
    foreach (var rejection in err.Rejections)
        foreach (var reason in rejection.Errors)
            Debug.LogWarning($"{rejection.MessageId}: {reason.Field} - {reason.Message} ({reason.Code})");
};
```

## FAQ

> [accordion] Which scripting backends are supported?
>
> Both **Mono** and **IL2CPP** are fully supported. Choose either in **Edit → Project Settings → Player → Other Settings → Scripting Backend**.

> [accordion] Does the SDK work in the Editor?
>
> Yes. Init, Track, session lifecycle, and flush all work in Play Mode and Edit Mode test runners. Events fired in the Editor reach the same pipeline as builds. Set `TestMode = true` in your `AudienceConfig` during development to mark events as test traffic and keep them separate from production data in analytics.

> [accordion] What happens if I call Track before consent is set?
>
> The call is a no-op. No event is queued, no warning is logged. Once `SetConsent(ConsentLevel.Anonymous)` (or higher) runs, subsequent Track calls flow normally. To verify state during development, read `ImmutableAudience.CurrentConsent` or the other [diagnostic getters](#diagnostic-getters).

> [accordion] Can I use the Unity SDK alongside the Web SDK?
>
> Yes. Unity captures in-game actions, and the Web SDK covers web games, marketing sites, and single-page apps. All events flow to the same pipeline. See the [Data Dictionary](https://docs.immutable.com/docs/products/audience/analytics/data-dictionary.md) for the full per-event schema.

> [accordion] How do I handle a GDPR erasure request?
>
> Erasure is server-side only: from your backend, call `DELETE /v1/audience/data` with the player's `userId` or `anonymousId`, authenticated with your secret API key. There is no in-game method for this, since the game client only ever holds a publishable key, which is public and can't prove ownership of the data being erased. See [Deleting User Data](https://docs.immutable.com/docs/products/audience/analytics/rest-api.md#deleting-user-data) for the request shape.
>
> After your backend confirms the erasure request was accepted, call `Reset` on the client so subsequent activity isn't linked to the erased user:
>
> ```csharp theme={null}
> ImmutableAudience.Reset();
> ```

> [accordion] Do I need to call Identify manually if I'm on Steam or Epic?
>
> Auto-collection fires at `Init` when consent is `Full` and the platform session is already active (Steam running, or EOS initialized and the player logged in). If consent is below `Full` at `Init`, or the platform session is not active yet, the SDK does not auto-identify. Call `Identify` yourself once the player grants full consent.

> [accordion] Where does the SDK store its data?
>
> Under `Application.persistentDataPath` in a folder named `imtbl_audience`. The folder contains the anonymous ID, the persisted consent level, and any queued events that haven't been flushed yet. Deleting it resets the SDK's local state on next launch.

> [accordion] How does identity work across the marketing site and the game?
>
> Call `Identify` with the same `userId` on both surfaces at login. Events from both sessions are attributed to that player automatically, no `Alias` call needed.
>
> `Alias` is only needed when the same player uses different provider IDs on different surfaces. For example, if the player is identified as a Steam user in-game but later links an email account, call `Alias` to tell the backend they are the same person. See [Identity Stitching](https://docs.immutable.com/docs/products/audience/analytics/data-dictionary.md#identity-stitching) in the Data Dictionary.

## Next Steps

> [card] [Sample app](https://github.com/immutable/unity-immutable-sdk/tree/main/examples/audience)
>
> Working Unity project with Init, consent, Track, and Identify wired up

> [card] [Attribution](https://docs.immutable.com/docs/products/attribution/overview.md)
>
> How tracking data powers player attribution and Hub reports

> [card] [Data Dictionary](https://docs.immutable.com/docs/products/audience/analytics/data-dictionary.md)
>
> Full reference of event schemas and consent levels

> [card] [Web SDK](https://docs.immutable.com/docs/products/audience/analytics/web-sdk.md)
>
> Typed SDK for web games, marketing sites, and SPAs

> [card] [REST API](https://docs.immutable.com/docs/products/audience/analytics/rest-api.md)
>
> Send events from your backend or game server

> [card] [Conversion Postbacks](https://docs.immutable.com/docs/products/attribution/postbacks/overview.md)
>
> Send attributed conversions back to ad networks to improve campaign optimisation

> [card] [API Keys](https://docs.immutable.com/docs/guides/advanced-setup/api-keys.md)
>
> Manage your publishable and secret keys


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.