# Unity SDK

> Show the offerwall in your Unity game on Android and iOS. Your server creates a short, single-use session, and the plugin opens the offerwall with it, so no key ever ships inside your game.

Source: https://docs.moonetix.com/unity-sdk

## How it works

1. When the player taps your Earn button, your game asks your own server for a session.
2. Your server calls `POST /v1/session` with your API key and the player’s ID, and passes the token to the game.
3. The game calls `Moonetix.Open(token)`. The plugin opens the offerwall in a native screen.
4. Rewards reach your server through your postback, exactly as with the offerwall link. See [Postbacks](https://docs.moonetix.com/postbacks).

A token works once and for 10 minutes. Create a new one each time the player opens the offerwall. Your API key and your link hash salt stay on your server, so nothing in your game can be used to open another player’s offerwall.

| Platform | Status |
| --- | --- |
| Android | Supported on every Android version your Unity version supports. The plugin uses the [Android SDK](https://docs.moonetix.com/android-sdk) inside. |
| iOS 13 or newer | Supported. The plugin uses the [iOS SDK](https://docs.moonetix.com/ios-sdk) inside. Build with Xcode 16 or newer. |
| Unity Editor, desktop, WebGL | Not supported. `Open` checks the app ID and the token and then reports `Unsupported`, so you can test your flow in the Editor. |

## Before you start

- An approved publisher account and an app. Every app you add is live right away.
- Two values from your dashboard, under Apps, your app, Integration. The **App ID** goes into your game, for `Moonetix.Init`. The **API key** stays on your server, for `POST /v1/session`. They are different values: never put the API key in your game.
- Your postback URL in the Postback tab, so rewards reach your server. See [Postbacks](https://docs.moonetix.com/postbacks).
- Your currency in the Currency tab, so players see the right rewards. See [Rewards and currency](https://docs.moonetix.com/rewards).

## Install

Unity 2022.3 LTS or Unity 6 (6000.0) or newer. The plugin is in the Moonetix package registry. Add the registry and the package to `Packages/manifest.json`:

```json
{
  "scopedRegistries": [
    {
      "name": "Moonetix",
      "url": "https://moonetix.com/upm",
      "scopes": ["com.moonetix"]
    }
  ],
  "dependencies": {
    "com.moonetix.offerwall": "2.0.0"
  }
}
```

You can also add the registry in Project Settings, Package Manager, and then install Moonetix Offerwall from My Registries in the Package Manager window. Prefer a file? Download [moonetix-unity-offerwall-2.0.0.tgz](https://moonetix.com/downloads/moonetix-unity-offerwall-2.0.0.tgz) and choose Install package from tarball in the Package Manager.

There is nothing else to set up:

- **Android:** when Unity generates the Gradle project, the plugin adds the Moonetix Maven repository (only for the `com.moonetix` group), the Android SDK and its R8 rule. This also works for exported projects and custom Gradle templates.
- **iOS:** the plugin brings the Moonetix iOS SDK with it and adds it to the Xcode project, linked and embedded. Set Target minimum iOS Version to 13.0 or higher in Player Settings; the build stops with a clear message otherwise.

## Create a session on your server

`POST https://api.moonetix.com/v1/session`

Send the fields as a form or as JSON, with your API key in the `Authorization` header. This is the same request as for the [Android SDK](https://docs.moonetix.com/android-sdk#session).

| Field | Description |
| --- | --- |
| `user_id` (required, string) | The same user ID you receive in postbacks. 1 to 100 characters: letters, numbers, and `. _ @ : + -`. |
| `sub1` to `sub5` (optional, string) | Your own tracking values. They are returned in the postback as `{sub1}` to `{sub5}`. |

Your game does not call this itself, because it needs the API key. Add one endpoint to your own server that checks which player is signed in, creates the session for that player, and returns only the token. The [token endpoint example](https://docs.moonetix.com/android-sdk#token-endpoint) on the Android page works for Unity too, in PHP, Node.js, and Python.

Errors: 400 `invalid_user_id`, 401 `invalid_api_key`, 403 `app_not_live` or `user_blocked` (do not show the offerwall), 405 `method_not_allowed`, 429 `rate_limited` (retry after the time in the `Retry-After` header).

## Open the offerwall

Call `Init` once with the App ID from the Integration tab. Then open the offerwall with a fresh token from your server.

```
using MoonetixOfferwall;
using UnityEngine;

public class EarnButton : MonoBehaviour
{
    void Awake()
    {
        Moonetix.Init("YOUR_APP_ID");
    }

    // Call this with a fresh token from your server.
    public void ShowOfferwall(string token)
    {
        Moonetix.Open(token);
    }
}
```

`Open` never throws for offerwall problems. They arrive as events, see [Events](https://docs.moonetix.com/unity-sdk#events). The package has an Offerwall demo sample with an Earn button that gets a token from your server: Window, Package Manager, Moonetix Offerwall, Samples.

## Full example

A complete Earn button component: it asks your server for a token with `UnityWebRequest`, opens the offerwall, and listens to the events. Add it to a GameObject, set Token Url to your token endpoint, and call `OpenOfferwall` from the On Click event of your button. Send your own sign-in credential so your server knows who the player is.

```csharp
using System;
using System.Collections;
using MoonetixOfferwall;
using UnityEngine;
using UnityEngine.Networking;

public class EarnButton : MonoBehaviour
{
    // Your token endpoint. It calls POST /v1/session with the API key.
    [SerializeField] private string tokenUrl = "https://your-server.example/api/moonetix/session";

    [Serializable]
    private class TokenResponse
    {
        public string token = "";
    }

    private bool _busy;

    void Awake()
    {
        Moonetix.Init("YOUR_APP_ID");
    }

    void OnEnable()
    {
        Moonetix.OnClosed += RefreshBalance;
        Moonetix.OnError += HandleError;
    }

    void OnDisable()
    {
        Moonetix.OnClosed -= RefreshBalance;
        Moonetix.OnError -= HandleError;
    }

    // Hook this up to the On Click event of your Earn button.
    public void OpenOfferwall()
    {
        if (!Moonetix.IsSupported)
        {
            Debug.Log("The offerwall opens only in Android and iOS builds.");
            return;
        }
        if (!_busy)
        {
            StartCoroutine(FetchTokenAndOpen());
        }
    }

    private IEnumerator FetchTokenAndOpen()
    {
        _busy = true;
        using (UnityWebRequest request = UnityWebRequest.Post(tokenUrl, new WWWForm()))
        {
            request.SetRequestHeader("Authorization", "Bearer " + PlayerAuthToken());
            request.timeout = 15;
            yield return request.SendWebRequest();
            _busy = false;
            if (request.result != UnityWebRequest.Result.Success)
            {
                Debug.LogWarning("Your server did not return a token: " + request.error);
                yield break;
            }
            TokenResponse response = null;
            try
            {
                response = JsonUtility.FromJson<TokenResponse>(request.downloadHandler.text);
            }
            catch (ArgumentException)
            {
            }
            if (response == null || string.IsNullOrEmpty(response.token))
            {
                Debug.LogWarning("Your server answered without a token.");
                yield break;
            }
            Moonetix.Open(response.token);
        }
    }

    // Your own sign-in credential, so your server knows who the player is.
    private string PlayerAuthToken()
    {
        return "your-own-sign-in-token";
    }

    // Load the player's balance from your server. Rewards arrive there by postback.
    private void RefreshBalance()
    {
    }

    private void HandleError(MoonetixError error)
    {
        Debug.LogWarning(error.CodeName + ": " + error.Message);
    }
}
```

Let your server take the player ID from its own sign-in, never from the request, so nobody can open the offerwall as another player. `Moonetix.IsSupported` is false in the Editor and on other platforms, so you can keep the button and test the flow in the Editor.

## Branding

The top bar of the offerwall screen shows the Moonetix mark, like the offerwall itself, with your offerwall title and colors from the Design tab of your dashboard. To set the title or color yourself, or to hide the mark, pass options:

```
Moonetix.Open(token, new MoonetixOptions(
    title: "Earn coins",
    toolbarColor: new Color32(0x00, 0x4A, 0xBB, 0xFF),
    showLogo: false));
```

## Events

All events are raised on the Unity main thread, so you can update your UI directly.

```
void OnEnable()
{
    Moonetix.OnClosed += RefreshBalance;
    Moonetix.OnError += HandleError;
}

void OnDisable()
{
    Moonetix.OnClosed -= RefreshBalance;
    Moonetix.OnError -= HandleError;
}

void HandleError(MoonetixError error)
{
    Debug.LogWarning(error.CodeName + ": " + error.Message);
}
```

| Event | When |
| --- | --- |
| `OnOpened` | The offerwall finished loading. On Android your game is paused while the offerwall is open, so this event can arrive when the player returns. |
| `OnClosed` | The player closed the offerwall. A good moment to load the player’s balance from your server. |
| `OnError` | `NotInitialized`, `InvalidAppId`, `InvalidToken`, `TokenExpiredOrUsed`, `Network`, `Unsupported`, or `Unknown`. `CodeName` gives the name the native SDKs use, for example `TOKEN_EXPIRED_OR_USED`. Create a new session and try again. |

## App Store and Google Play privacy

The iOS SDK inside the plugin has a privacy manifest that lists what the offerwall collects, as described in our [privacy policy](https://moonetix.com/privacy). In App Store Connect, under App Privacy, and in Google Play’s Data safety form, add these data types for your game:

| Data type | Linked to the user | Used for tracking | Purpose |
| --- | --- | --- | --- |
| User ID | Yes | No | App Functionality |
| Device ID | Yes | No | App Functionality |
| Coarse Location | Yes | No | App Functionality |
| Product Interaction | Yes | No | App Functionality |

The user ID is the pseudonymous ID your server sends with the session, the device ID is used for fraud prevention, the coarse location comes from the IP address, and product interaction is the offers a player views, starts, and completes. The SDK does not track players across apps and does not use the advertising identifier. In Google Play’s Data safety form, the same data types are called User IDs, Device or other IDs, Approximate location, and App interactions.

## Security

- Create sessions on your server only. Never put the API key or the link hash salt in your game, because anyone can read a game’s files.
- Credit rewards only from the signed postback on your server, never from events in the game. Events can be faked on a player’s device.
- Make sure your own endpoint that returns tokens only does so for the signed-in player.

## Testing

Before you launch, create a session from your server for a test user ID and open it in a development build on an Android phone, an iPhone, or the iOS Simulator. Check that the offerwall opens, that an offer opens in the store or the browser, and that the back and close buttons work. Then send test postbacks from Integration status on the Integration tab of your app to check your server. Test on a normal home or mobile connection: on a VPN or a test device in the cloud, the offerwall shows a block page such as VPN detected instead of offers. See [Testing](https://docs.moonetix.com/testing).

Clicks that come through the plugin show as `sdk` in the `{integration}` postback macro and in the [Reporting API](https://docs.moonetix.com/reporting-api), and as Mobile SDK in your dashboard, like the Android and iOS SDKs.

## Troubleshooting

| What you see | Cause and fix |
| --- | --- |
| `Unsupported` | The game runs in the Unity Editor, on desktop, or in WebGL. **Fix:** Build for Android or iOS and test on a phone or the iOS Simulator. Check `Moonetix.IsSupported` before you show the button. |
| `InvalidAppId` | The value in `Moonetix.Init` is not the App ID. It must be the 24-character lowercase hex value from the Integration tab. Passing the API key is a common mistake. **Fix:** Copy the App ID again from the Integration tab. |
| `NotInitialized` | `Open` was called before `Init`. **Fix:** Call `Moonetix.Init` in `Awake` of your first scene. |
| `InvalidToken` right after `Open` | The value is not the token, for example the whole JSON answer or an empty string. **Fix:** Pass only the `token` field, which starts with `sst_`. |
| `InvalidToken` after the screen opened | We do not know the token. Usually it was created with the API key of another app than the App ID in `Init`. **Fix:** Use the App ID and the API key of the same app. |
| `TokenExpiredOrUsed`, or a Session ended page | The token is older than 10 minutes or was used already, for example because the game opened it twice or kept it from an earlier tap. **Fix:** Create a new token for every `Open`, right when the player taps Earn. |
| `Network`, and a No connection page with a Try again button | The device could not reach the offerwall. **Fix:** The player can tap Try again once the connection is back. |
| The iOS build stops with a message about the iOS version | Target minimum iOS Version in Player Settings is below 13.0. **Fix:** Set it to 13.0 or higher. |
| Your server gets 403 from `/v1/session` | `app_not_live`: the app is paused or your account is not active. `user_blocked`: we blocked this player. **Fix:** Do not show the offerwall. For `app_not_live`, check your notifications. |
| VPN detected, Proxy detected, or Network not supported | The device is on a VPN, a proxy, or a cloud network. **Fix:** Test on a normal home or mobile connection. See [Test from a normal connection](https://docs.moonetix.com/testing#vpn). |

For problems on one platform only, the [Android](https://docs.moonetix.com/android-sdk#troubleshooting) and [iOS](https://docs.moonetix.com/ios-sdk#troubleshooting) pages list what the native SDKs report.

## Changelog

**2.0.0**: the SDK moves to the Moonetix name: `com.moonetix.offerwall`, namespace `MoonetixOfferwall`, class `Moonetix`. The wall opens on `wall.moonetix.com`. The old package stays published.

**1.0.0**: first release. `Moonetix.Init` and `Moonetix.Open` with the same checks and error codes as the Android and iOS SDKs, main-thread events, top bar options, the Android SDK 1.1.0 added to Gradle automatically, the iOS SDK 1.0.1 bundled and embedded automatically, and the Offerwall demo sample.

## License

The plugin is free to use under the Apache License 2.0. The license file is in the package.

## Quick reference

### Packages/manifest.json

```
"scopedRegistries": [
  {
    "name": "Moonetix",
    "url": "https://moonetix.com/upm",
    "scopes": ["com.moonetix"]
  }
],
"dependencies": {
  "com.moonetix.offerwall": "2.0.0"
}
```

### Create a session

`POST api.moonetix.com/v1/session`

```bash
curl -X POST "https://api.moonetix.com/v1/session" \
  -H "Authorization: Bearer $MOONETIX_API_KEY" \
  -d "user_id=player_1234"
```

```php
$ch = curl_init('https://api.moonetix.com/v1/session');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query(['user_id' => $playerId]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);
$token = json_decode(curl_exec($ch), true)['token'] ?? null;
```

```javascript
const res = await fetch('https://api.moonetix.com/v1/session', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MOONETIX_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ user_id: playerId }),
});
const { token } = await res.json();
```

Your API key is in your dashboard under Apps, your app, Integration. Up to 600 requests per minute for each app.

Questions? [partners@moonetix.com](mailto:partners@moonetix.com)
