MoonetixMoonetixDocs Support Log in

DocsMobile SDKs

View as Markdown

Android SDK

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

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(activity, 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.

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.init. 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.
  • Your currency in the Currency tab, so users see the right rewards. See Rewards and currency.

Install

Android 5.0 (API 21) or newer, with Android System WebView or Chrome on the device. The only dependency is the Kotlin standard library, which Gradle adds by itself, so the SDK works in Kotlin and in Java apps. Add the Moonetix Maven repository and the dependency:

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://moonetix.com/maven") }
    }
}

// app/build.gradle.kts
dependencies {
    implementation("com.moonetix:offerwall:2.0.0")
}
// settings.gradle
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url 'https://moonetix.com/maven' }
    }
}

// app/build.gradle
dependencies {
    implementation 'com.moonetix:offerwall:2.0.0'
}
  • Older projects list their repositories under allprojects in the top-level build.gradle. Add the Moonetix line there instead.
  • The SDK adds the internet permission and its offerwall screen to your app’s manifest by itself.
  • It brings its own R8 and ProGuard rules, so you do not need to add any.

Create a session on your server

POSThttps://api.moonetix.com/v1/session

Send the fields as a form or as JSON, with your API key in the Authorization header. You find the key under Apps, your app, Integration.

FieldDescription
user_idrequiredstringThe same user ID you receive in postbacks. 1 to 100 characters: letters, numbers, and . _ @ : + -.
sub1 to sub5optionalstringYour own tracking values. They are returned in the postback as {sub1} to {sub5}. A value with characters we do not accept is left out.
StatuserrorMeaning
201The session was created. Pass token to your app.
400invalid_user_iduser_id is missing or has characters that are not allowed.
401invalid_api_keyThe API key is missing or wrong.
403app_not_live, user_blockedThe app is paused, or this user is blocked. Do not show the offerwall.
405method_not_allowedUse POST.
429rate_limitedMore than 600 requests in a minute for this app. Retry after the time in the Retry-After header.

Your token endpoint

Your app never calls /v1/session itself, because that needs the API key. Add one endpoint to your own server instead, for example POST /api/moonetix/session. It checks who is signed in, asks us for a session for that user, and returns only the token:

<?php
// POST /api/moonetix/session: a fresh offerwall token for the signed-in user.
$user = current_user();
if (!$user) {
    http_response_code(401);
    exit;
}

$ch = curl_init('https://api.moonetix.com/v1/session');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query(['user_id' => (string)$user->id]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('MOONETIX_API_KEY')],
]);
$body = curl_exec($ch);
$code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode((string)$body, true);

header('Content-Type: application/json');
if ($code === 201 && !empty($data['token'])) {
    echo json_encode(['token' => $data['token']]);
    exit;
}
error_log('Moonetix session failed: HTTP ' . $code . ' ' . ($data['error'] ?? ''));
http_response_code($code === 403 ? 403 : 503);
echo json_encode(['error' => 'offerwall_unavailable']);
app.post('/api/moonetix/session', requireLogin, async (req, res) => {
  try {
    const r = 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: String(req.user.id) }),
      signal: AbortSignal.timeout(10000),
    });
    const data = await r.json().catch(() => ({}));
    if (r.status === 201 && data.token) {
      return res.json({ token: data.token });
    }
    console.warn('Moonetix session failed', r.status, data.error);
    return res.status(r.status === 403 ? 403 : 503).json({ error: 'offerwall_unavailable' });
  } catch (e) {
    return res.status(503).json({ error: 'offerwall_unavailable' });
  }
});
import os, requests
from flask import Flask
from flask_login import current_user, login_required

app = Flask(__name__)

@app.post("/api/moonetix/session")
@login_required
def moonetix_session():
    try:
        r = requests.post(
            "https://api.moonetix.com/v1/session",
            data={"user_id": str(current_user.id)},
            headers={"Authorization": f"Bearer {os.environ['MOONETIX_API_KEY']}"},
            timeout=10,
        )
        data = r.json()
    except (requests.RequestException, ValueError):
        return {"error": "offerwall_unavailable"}, 503
    if r.status_code == 201 and data.get("token"):
        return {"token": data["token"]}
    app.logger.warning("Moonetix session failed: %s %s", r.status_code, data.get("error"))
    return {"error": "offerwall_unavailable"}, 403 if r.status_code == 403 else 503
  • Take the user ID from your own sign-in, never from the request. Otherwise anyone could open the offerwall as someone else.
  • Return only the token. Never pass the API key or our full response on to the app.
  • If we answer 403, do not show the offerwall. The example answers 403 too, so your app can hide its Earn button for that user.
  • Do not keep tokens for later. Create a new one each time the user taps Earn.

Open the offerwall

Call init once, for example in your Application class, with the App ID from the Integration tab. Then open the offerwall with the token from your server. Call all Moonetix methods from the main thread.

// Kotlin
Moonetix.init(applicationContext, "YOUR_APP_ID")
Moonetix.open(activity, token)

// your own title, see Branding below
Moonetix.open(activity, token, MoonetixOptions(title = "Earn coins"))
// Java
Moonetix.init(getApplicationContext(), "YOUR_APP_ID");
Moonetix.open(activity, token);

open never throws for offerwall problems. They arrive as events, see Events.

Full example

The whole app side: the SDK is set up when the app starts, a tap on Earn asks your server for a token in the background, and the offerwall opens with it. The examples use Kotlin coroutines and lifecycleScope from androidx.lifecycle:lifecycle-runtime-ktx, which most apps already have.

Set up the SDK once, in your Application class. Register it in your manifest with android:name if you do not have one yet:

import android.app.Application
import com.moonetix.offerwall.Moonetix

class EarnApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Moonetix.init(this, "YOUR_APP_ID")
    }
}

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:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL

// Asks YOUR server for a fresh token. Your server calls POST /v1/session with the API key.
suspend fun fetchMoonetixToken(authToken: String): String = withContext(Dispatchers.IO) {
    val conn = URL("https://your-server.example/api/moonetix/session").openConnection() as HttpURLConnection
    try {
        conn.requestMethod = "POST"
        conn.connectTimeout = 10_000
        conn.readTimeout = 10_000
        conn.setRequestProperty("Authorization", "Bearer $authToken")
        conn.doOutput = true
        conn.outputStream.close()
        if (conn.responseCode != 200) {
            throw IOException("Token request failed: HTTP ${conn.responseCode}")
        }
        val body = conn.inputStream.bufferedReader().use { it.readText() }
        JSONObject(body).getString("token")
    } finally {
        conn.disconnect()
    }
}

Open the offerwall from a button, in a classic Activity or in Jetpack Compose:

import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.moonetix.offerwall.Moonetix
import com.moonetix.offerwall.MoonetixError
import com.moonetix.offerwall.MoonetixListener
import kotlinx.coroutines.launch

class EarnActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_earn)

        Moonetix.setListener(object : MoonetixListener {
            override fun onClosed() {
                refreshBalance()
            }

            override fun onError(error: MoonetixError) {
                Log.w("Earn", "${error.code}: ${error.message}")
            }
        })

        findViewById<Button>(R.id.earn_button).setOnClickListener { openOfferwall() }
    }

    override fun onDestroy() {
        Moonetix.setListener(null)
        super.onDestroy()
    }

    private fun openOfferwall() {
        lifecycleScope.launch {
            val token = try {
                fetchMoonetixToken(currentUserAuthToken())
            } catch (e: Exception) {
                Toast.makeText(this@EarnActivity, "The offerwall is not available right now.", Toast.LENGTH_SHORT).show()
                return@launch
            }
            Moonetix.open(this@EarnActivity, token)
        }
    }

    // Your own sign-in credential, so your server knows who the user is.
    private fun currentUserAuthToken(): String = "your-own-sign-in-token"

    // Load the user's balance from your server. Rewards arrive there by postback.
    private fun refreshBalance() {}
}
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import com.moonetix.offerwall.Moonetix
import kotlinx.coroutines.launch

@Composable
fun EarnButton(authToken: String) {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()
    Button(onClick = {
        scope.launch {
            val token = runCatching { fetchMoonetixToken(authToken) }.getOrNull() ?: return@launch
            context.findActivity()?.let { Moonetix.open(it, token) }
        }
    }) {
        Text("Earn coins")
    }
}

fun Context.findActivity(): Activity? {
    var c = this
    while (c is ContextWrapper) {
        if (c is Activity) return c
        c = c.baseContext
    }
    return null
}
  • Set the listener in onCreate and clear it in onDestroy, not in onStop. The offerwall covers your screen, so onStop runs while it is open, and you would miss onClosed.
  • In Compose, Moonetix.open needs the Activity. findActivity() gets it from LocalContext.

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, so it matches your offerwall without any code. When you change them in the dashboard, the SDK picks them up within a few minutes.

To set the title or color yourself, or to hide the mark, pass options. Anything you set wins over the Design tab:

Moonetix.open(activity, token, MoonetixOptions(
    title = "Earn coins",
    toolbarColor = 0xFF004ABB.toInt(),
    showLogo = false,
))

Events

Moonetix.setListener(object : MoonetixListener {
    override fun onOpened() { }
    override fun onClosed() { refreshBalance() }
    override fun onError(error: MoonetixError) { Log.w("Earn", error.code.name) }
})
Moonetix.setListener(new MoonetixListener() {
    @Override
    public void onClosed() {
        refreshBalance();
    }

    @Override
    public void onError(MoonetixError error) {
        Log.w("Earn", error.getCode().name() + ": " + error.getMessage());
    }
});
EventWhen
onOpenedThe offerwall finished loading.
onClosedThe user closed the offerwall. A good moment to load the user’s balance from your server.
onErrorNOT_INITIALIZED, INVALID_APP_ID, INVALID_TOKEN, TOKEN_EXPIRED_OR_USED, NETWORK, or UNKNOWN. Create a new session and try again. What each one means: Troubleshooting.

All events arrive on the main thread, and every method has an empty default, so you only override what you need, in Kotlin and in Java. The listener is kept in a static field: set it from a long-lived object, such as your Application, or clear it with Moonetix.setListener(null) when your screen goes away. Events are for your user interface only: credit rewards from the postback, never from an event.

What the SDK handles for you

  • Offers that lead to Google Play open in the Play Store app, and other sites open in the browser, so installs are tracked correctly.
  • The back button goes back inside the offerwall, and the close button returns to your app.
  • Screen rotation and low-memory restarts keep the user where they were, without using the token again.
  • A locked-down WebView: no file access, no mixed content, Safe Browsing on, and no JavaScript bridge into your app.

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. Check that the offerwall opens, that an offer opens in the Play 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.

Clicks that come through the SDK show as sdk in the {integration} postback macro and in the Reporting API, and as Mobile SDK in your dashboard.

Troubleshooting

What you seeCause and fix
INVALID_APP_IDThe 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.
NOT_INITIALIZEDopen was called before init. Fix: Call init in Application.onCreate.
INVALID_TOKEN right after openThe 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 openedWe 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.
TOKEN_EXPIRED_OR_USED, or a Session ended pageThe 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. It also happens when Android closed your app while the offerwall was loading for the first time. Fix: Create a new token for every open, right when the user taps Earn.
NETWORK, and a No connection page with a Try again buttonThe device could not reach the offerwall. Fix: The user can tap Try again once the connection is back.
UNKNOWNAndroid System WebView is missing or turned off, the Activity you passed was already closing, or the screen could not start. Fix: Ask the user to update or turn on Android System WebView in Google Play. Pass an Activity that is on screen.
Your server gets 403 from /v1/sessionapp_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 supportedThe 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.
Few or no offersOffers depend on the user’s country and device. Some run only on iOS, or only in some countries. Fix: Test from a country where offers run, and on an Android phone.
onClosed never arrivesThe listener was cleared in onStop, or it belonged to a screen that is gone. Fix: Set it in onCreate and clear it in onDestroy, or set it once in your Application.

Google Play Data safety

Google Play asks every app to declare the data it collects, including data collected by SDKs. The offerwall collects the data below, as described in our privacy policy. In Play Console, under App content, Data safety, add these data types for your app:

Data type in Play ConsoleWhat it is
User IDsThe pseudonymous user ID your server sends with the session.
Device or other IDsA device identifier we use for fraud prevention.
Approximate locationThe country, region, and city, from the IP address.
App interactionsThe offers a user views, starts, and completes.

All of them are used for the offerwall to work and to prevent fraud, not for advertising. The SDK does not use the advertising ID, does not ask for any permission other than internet access, and does not track users across apps. Publishing on iOS too? See App Store privacy details.

Changelog

  • 2.0.0: the SDK moves to the Moonetix name: com.moonetix:offerwall, package com.moonetix.offerwall, class Moonetix. The wall opens on wall.moonetix.com. The old package stays published.
  • 1.1.0: the top bar shows the Moonetix mark with the title and colors from the Design tab of your dashboard, without any code. New showLogo option. A title or color you set in MoonetixOptions still wins.
  • 1.0.0: first release. Moonetix.init, Moonetix.open, and Moonetix.setListener with opened, closed, and error events, single-use session tokens, and MoonetixOptions with title and toolbarColor.

License

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