# iOS SDK

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

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

## How it works

1. When the user taps your Earn button, your app asks your own server for a session.
2. Your server calls `POST /v1/session` with your API key and the user’s ID, and passes the token to the app.
3. The app calls `Moonetix.open(token:)`. The SDK 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 user opens the offerwall. Your API key and your link hash salt stay on your server, so nothing in your app can be used to open another user’s offerwall.

## 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 app, for `Moonetix.configure`. The **API key** stays on your server, for `POST /v1/session`. They are different values: never put the API key in your app.
- 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 users see the right rewards. See [Rewards and currency](https://docs.moonetix.com/rewards).

## Install

iOS 13 or newer, iPhone and iPad, built with Xcode 16 or newer. The SDK uses only UIKit and WebKit and has no third-party dependencies. It works from Swift, SwiftUI and Objective-C.

### Swift Package Manager

In Xcode, choose File, Add Package Dependencies, enter `https://moonetix.com/ios/moonetix-ios.git` and add the `MoonetixOfferwall` library to your app target. In a `Package.swift`:

```
dependencies: [
    .package(url: "https://moonetix.com/ios/moonetix-ios.git", from: "2.0.0"),
],
targets: [
    .target(name: "MyApp", dependencies: [
        .product(name: "MoonetixOfferwall", package: "moonetix-ios"),
    ]),
]
```

### CocoaPods

```
pod 'MoonetixOfferwall', :podspec => 'https://moonetix.com/ios/2.0.0/MoonetixOfferwall.podspec'
```

### Manual

Download [MoonetixOfferwall-2.0.0.xcframework.zip](https://moonetix.com/ios/MoonetixOfferwall-2.0.0.xcframework.zip), unzip it, drag `MoonetixOfferwall.xcframework` into your project and set it to Embed & Sign.

## 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 app does not call this itself, because it needs the API key. Add one endpoint to your own server that checks who is signed in, creates the session for that user, and returns only the token. The [token endpoint example](https://docs.moonetix.com/android-sdk#token-endpoint) on the Android page works for iOS 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 `configure` once, for example in `application(_:didFinishLaunchingWithOptions:)`, with the App ID from the Integration tab. Then open the offerwall with a fresh token from your server.

```
import MoonetixOfferwall

Moonetix.configure(appId: "YOUR_APP_ID")

// When the user taps Earn, with a token from your server:
Moonetix.open(token: token)

// Or present from a view controller of your choice:
Moonetix.open(token: token, from: self)
```

Without a view controller, the SDK presents from the top screen of your app, so it also works from SwiftUI:

```
Button("Earn coins") {
    Task {
        let token = try await api.fetchMoonetixToken()
        Moonetix.open(token: token)
    }
}
```

Objective-C:

```
@import MoonetixOfferwall;

[Moonetix configureWithAppId:@"YOUR_APP_ID"];
[Moonetix openWithToken:token from:self options:nil];
```

To close the offerwall from your own code, for example when the user signs out, call `Moonetix.close()`. The close event is reported as usual. Call Moonetix from the main thread. `open` and `close` also accept calls from other threads and move them to the main thread by themselves.

`open` never throws for offerwall problems. They arrive as events, see [Events](https://docs.moonetix.com/ios-sdk#events).

## Full example

The whole app side: a tap on Earn asks your server for a token, and the offerwall opens with it. Call `Moonetix.configure(appId:)` once when your app starts, as shown above.

Ask your token endpoint for a token. Replace the URL with yours, and send your own sign-in credential so your server knows who the user is. This version uses async/await, which needs iOS 15. On iOS 13 and 14, use `URLSession.dataTask` with a completion handler instead.

```swift
import Foundation

struct MoonetixTokenError: Error {}

// Asks YOUR server for a fresh token. Your server calls POST /v1/session with the API key.
func fetchMoonetixToken(authToken: String) async throws -> String {
    var request = URLRequest(url: URL(string: "https://your-server.example/api/moonetix/session")!)
    request.httpMethod = "POST"
    request.timeoutInterval = 10
    request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
    let (data, response) = try await URLSession.shared.data(for: request)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else {
        throw MoonetixTokenError()
    }
    struct Body: Decodable { let token: String }
    return try JSONDecoder().decode(Body.self, from: data).token
}
```

Open the offerwall from a button in UIKit or in SwiftUI:

```swift
import UIKit
import MoonetixOfferwall

final class EarnViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        Moonetix.onClosed = { [weak self] in
            self?.refreshBalance()
        }
        Moonetix.onError = { error in
            print("Moonetix:", error.code.name, error.message)
        }
    }

    @IBAction func earnTapped(_ sender: UIButton) {
        Task {
            do {
                let token = try await fetchMoonetixToken(authToken: currentUserAuthToken())
                Moonetix.open(token: token, from: self)
            } catch {
                let alert = UIAlertController(title: nil, message: "The offerwall is not available right now.", preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "OK", style: .default))
                present(alert, animated: true)
            }
        }
    }

    // Your own sign-in credential, so your server knows who the user is.
    private func currentUserAuthToken() -> String { "your-own-sign-in-token" }

    // Load the user's balance from your server. Rewards arrive there by postback.
    private func refreshBalance() {}
}
```

```swift
import SwiftUI
import MoonetixOfferwall

struct EarnButton: View {
    let authToken: String

    var body: some View {
        Button("Earn coins") {
            Task {
                guard let token = try? await fetchMoonetixToken(authToken: authToken) else { return }
                Moonetix.open(token: token)
            }
        }
    }
}
```

`Moonetix.onClosed` and `Moonetix.onError` are kept until you replace them. Set them once, for example where you call `configure`, or capture your screen weakly as above.

## 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. When you change them in the dashboard, the SDK picks them up within a few minutes. To set them yourself, pass options. Anything you set wins over the Design tab:

```
Moonetix.open(token: token, options: MoonetixOptions(
    title: "Earn coins",
    toolbarColor: UIColor(red: 0, green: 0.29, blue: 0.73, alpha: 1),
    showLogo: false
))
```

The offerwall opens full screen. Pass `presentation: .sheet` to open it as a sheet the user can also swipe down to close.

## Events

```
Moonetix.onClosed = { [weak self] in
    self?.refreshBalance()
}
Moonetix.onError = { error in
    print(error.code.name, error.message)
}
```

Prefer a delegate? Set `Moonetix.delegate` (a weak reference) to an object that adopts `MoonetixDelegate`: `moonetixDidOpen()`, `moonetixDidClose()` and `moonetixDidFail(with:)`. All events arrive on the main thread.

| Event | When |
| --- | --- |
| `onOpened` | The offerwall finished loading. |
| `onClosed` | The user closed the offerwall. A good moment to load the user’s balance from your server. |
| `onError` | `NOT_INITIALIZED`, `INVALID_APP_ID`, `INVALID_TOKEN`, `TOKEN_EXPIRED_OR_USED`, `NETWORK`, or `UNKNOWN`. Create a new session and try again. |

Using Flutter, React Native, or Unity? The [Flutter SDK](https://docs.moonetix.com/flutter-sdk), the [React Native SDK](https://docs.moonetix.com/react-native-sdk), and the [Unity SDK](https://docs.moonetix.com/unity-sdk) bring this SDK with them.

## App Store privacy details

The SDK’s privacy manifest lists what the offerwall collects, as described in our [privacy policy](https://moonetix.com/privacy). In App Store Connect, under App Privacy, add these data types for your app:

| 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 user views, starts, and completes. The SDK does not track users across apps and does not use the advertising identifier. Publishing on Android? Fill in Google Play’s Data safety form with the same data types.

## What the SDK handles for you

- Offers that lead to the App Store open in the App Store app, and other sites open in Safari, so installs are tracked correctly.
- The close button returns to your app. Swiping from the left edge goes back inside the offerwall.
- If iOS pauses the offerwall in the background, it comes back where the user was, without using the token again.
- A locked-down web view: only offerwall pages load inside it, no JavaScript bridge into your app, and camera and microphone requests are denied.
- No device fingerprinting: inside the SDK the offerwall reads no device signals. Fraud checks use a random ID the offerwall creates itself, plus server-side checks.
- Light and dark mode, larger text sizes and VoiceOver.

## Security

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

## Testing

Before you launch, create a session from your server for a test user ID and open it in a debug build of your app on an iPhone or in the Simulator. Check that the offerwall opens, that an offer opens in the App Store or Safari, and that the close button works. 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 SDK 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 SDK.

## Troubleshooting

| What you see | Cause and fix |
| --- | --- |
| `INVALID_APP_ID` | The value in `Moonetix.configure(appId:)` 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. |
| `NOT_INITIALIZED` | `open` was called before `configure`. **Fix:** Call `configure` when your app starts. |
| `INVALID_TOKEN` 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_`. |
| `INVALID_TOKEN` 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 `configure`. **Fix:** Use the App ID and the API key of the same app. |
| `TOKEN_EXPIRED_OR_USED`, or a Session ended page | The token is older than 10 minutes or was used already, for example because the app opened it twice or kept it from an earlier tap. **Fix:** Create a new token for every `open`, right when the user taps Earn. |
| `NETWORK`, and a No connection page with a Try again button | The device could not reach the offerwall. **Fix:** The user can tap Try again once the connection is back. |
| `UNKNOWN` | The offerwall is already open, no view controller was on screen to present from, or the offerwall stopped unexpectedly. **Fix:** Open it once at a time. If your app shows another screen at that moment, pass a visible view controller with `open(token:from:)`. |
| 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 user. **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). |
| Few or no offers | Offers depend on the user’s country and device. Some run only on Android, or only in some countries. **Fix:** Test from a country where offers run, and on an iPhone or iPad. |
| Delegate methods are never called | `Moonetix.delegate` is a weak reference, so the object was already released. **Fix:** Keep a strong reference to your delegate, or use the closures. |

## Changelog

- **2.0.0**: the SDK moves to the Moonetix name: `MoonetixOfferwall`, class `Moonetix`, accessibility identifiers `moonetix.close` and `moonetix.title`. The wall opens on `wall.moonetix.com`. The old package stays published.
- **1.0.1**: the privacy manifest declares what the offerwall collects, for the App Store privacy details. Accessibility identifiers `moonetix.close` and `moonetix.title` for your UI tests.
- **1.0.0**: first release. `Moonetix.configure(appId:)`, `Moonetix.open(token:from:options:)`, `Moonetix.close()`, delegate and closure events, the same error codes as the Android SDK, branding from the Design tab, and full screen or sheet presentation. iOS 13 or newer.

## License

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

## Quick reference

### Swift package URL

```
https://moonetix.com/ios/moonetix-ios.git
```

### 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=user_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' => $userId]),
    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: userId }),
});
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)
