How to Add an Offerwall to an Android App (Kotlin)

Key takeaways

  • Install com.sharklio:offerwall:1.1.0 from the Sharklio Maven repository; it needs Android 5.0 (API 21) or newer and has no third-party dependencies.
  • Your server creates a single-use session token with POST /v1/session each time the user opens the wall, so the API key never ships inside the APK.
  • The app only opens the wall and reloads the balance; coins are added by your server from verified status 1 postbacks and taken back on status 2.

How do you add an offerwall to an Android app in Kotlin?

To add an offerwall to an Android app in Kotlin, add the offerwall SDK with Gradle, call Sharklio.init once with your App ID, and, when the user taps your Earn button, ask your own server for a session token and pass it to Sharklio.open(activity, token). Your server gets that token from POST https://api.sharklio.com/v1/session with your API key. Rewards never travel through the app: they reach your server as signed postbacks, and your server credits the user’s balance.

This guide follows the Sharklio Android SDK docs step by step, with Kotlin for the app and Node.js for the server. Where to put the wall and how to price your currency are covered in Monetize a mobile game with an offerwall. If you are new to the term, what an SDK is explains it in two minutes.

Who does what

Three parts work together. Keeping each one in its lane is what makes the integration safe:

PartDoesNever does
Your Android appInitializes the SDK, asks your server for a token, opens the wall, reloads the balance when it closesHolds the API key, the link hash salt, or the postback secret key; adds coins by itself
Your serverCreates a session for the signed-in user, receives postbacks, owns the walletTrusts the app about rewards; hands out tokens for other users
SharklioShows offers for the user’s country and device, tracks results, sends a signed postback for every credit and reversalPays your users directly

The App ID is the only Sharklio value inside your app, and it is not a secret. You find it, the API key, and the postback secret key in your dashboard under Apps, your app, Integration.

Step 1: install the SDK with Gradle

The SDK supports Android 5.0 (API 21) or newer, which is minSdk = 21 in your module, and has no third-party dependencies. Add the Sharklio Maven repository next to Google’s and Maven Central, then add the dependency:

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

// app/build.gradle.kts
dependencies {
    implementation("com.sharklio:offerwall:1.1.0")
}

Android’s Remote repositories guide explains that Gradle searches the repositories in the dependencyResolutionManagement block in the order you list them, and that new projects already list Google’s Maven repository and Maven Central. You only add the third line.

The SDK brings its own R8 and ProGuard rules, so a minified release build needs no extra keep rules. Still, test the release build once before you ship, not only the debug build.

Step 2: create a session on your server

The app never talks to the Sharklio API with a key. Instead, your server calls POST https://api.sharklio.com/v1/session with your API key in the Authorization header and the user’s ID, and returns the token to the app. Here is the call from the docs inside an Express route that sits behind your normal login check:

app.post('/offerwall-token', requireAuth, async (req, res) => {
  const userId = String(req.user.id);
  const r = await fetch('https://api.sharklio.com/v1/session', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.SHARKLIO_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ user_id: userId }),
  });
  if (r.status !== 201) return res.status(503).json({ error: 'offerwall_unavailable' });
  const { token } = await r.json();
  res.json({ token });
});

requireAuth stands for your own session middleware. The important part is that userId comes from the signed-in session, never from a parameter the app sends, so nobody can request a token for another user. PHP and Python versions of the same call are in the Create a session section of the docs.

Rules for the session

  • One token, one opening. A token works once and for 10 minutes (expires_in is 600). Create a new one every time the user opens the wall instead of caching it.
  • Use your account ID. user_id is the same ID you get back in postbacks: 1 to 100 characters from letters, numbers, and . _ @ : + -. Use the ID your wallet uses, not a device ID that changes on reinstall.
  • Optional sub IDs. Send sub1 to sub5 to tag a source, such as the screen the button sits on. They come back in the postback as {sub1} to {sub5}. See click IDs and sub IDs.
  • Respect a 403. app_not_live or user_blocked means do not show the wall. A 429 (rate_limited, more than 600 requests a minute for the app) means retry after the Retry-After header.

Step 3: initialize and open the wall in Kotlin

Call init once with the App ID from the Integration tab. The docs suggest your Application class, which runs before any screen, and it is also a long-lived place for the event listener you add in step 4:

import android.app.Application
import com.sharklio.offerwall.Sharklio

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Sharklio.init(applicationContext, "YOUR_APP_ID")
    }
}

Register the class with android:name=".MyApp" on the <application> tag of your manifest if you do not have one yet. Then open the wall from the screen with your Earn button:

class ShopActivity : AppCompatActivity() {

    private fun onEarnClicked() {
        lifecycleScope.launch {
            val token = backend.fetchOfferwallToken()
            if (token == null) {
                showMessage("Offers are not available right now.")
                return@launch
            }
            Sharklio.open(this@ShopActivity, token)
        }
    }
}

backend.fetchOfferwallToken() stands for your own suspend function that calls the /offerwall-token route from step 2 with the user’s session, using whatever HTTP client your app already has, and returns null on any error. showMessage is your own UI. lifecycleScope comes from androidx.lifecycle:lifecycle-runtime-ktx; Android’s coroutines with lifecycle-aware components page notes that coroutines in this scope are canceled when the lifecycle is destroyed, so a slow network call does not open the wall on a screen that is gone.

Match the wall to your app

The top bar of the wall shows the Sharklio mark with your offerwall title and colors from the Design tab of your dashboard, with no code. The SDK picks up changes within a few minutes. To set the title or color in code, or to hide the mark, pass options, which win over the Design tab:

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

Colors, radius, and fonts of the wall itself are set in the dashboard, with a live preview. See how to customize your offerwall design.

Step 4: handle events and the return to your app

The SDK tells you when the wall loads, closes, or fails. Set the listener from a long-lived object such as your Application, or clear it with Sharklio.setListener(null) when your screen goes away:

Sharklio.setListener(object : SharklioListener {
    override fun onOpened() { }
    override fun onClosed() { refreshBalance() }
    override fun onError(error: SharklioError) { Log.w("Earn", error.code.name) }
})

onClosed is the moment to reload the user’s balance from your server. It does not mean an offer was completed, and some offers credit minutes or days later, after the user reaches a later step. A short notice such as “Your 450 coins have arrived” when a credit lands brings users back to spend them.

What you seeCauseWhat to do
NOT_INITIALIZEDopen ran before initCall Sharklio.init in Application.onCreate
INVALID_APP_IDThe App ID is wrongCopy it again from the Integration tab
INVALID_TOKEN, TOKEN_EXPIRED_OR_USEDThe token is malformed, older than 10 minutes, or already usedCreate a new session and try again
NETWORK, UNKNOWNThe connection failedCreate a new session and try again
403 from /v1/sessionapp_not_live or user_blockedHide the Earn button for now

Why use the SDK instead of your own WebView?

The offerwall also opens from a signed link, so an Android app can show it in its own WebView or the browser. Here is what each path leaves to you:

Sharklio Android SDKYour own WebView with the linkSystem browser with the link
What your server makesA single-use session tokenA signed link (hash with the link salt)A signed link
Play Store offersOpen in the Play Store appYou intercept them and hand them to the systemHandled by the browser
Back and closeBack goes back inside the wall, close returns to your appYou build bothThe user switches back by hand
Rotation and low-memory restartsUser stays in place without reusing the tokenYou handle stateNot your concern
WebView settingsLocked down: no file access, no mixed content, Safe Browsing on, no JavaScript bridgeYou configure themNone

If you build your own WebView, remember two points from Android’s Build web apps in WebView guide: JavaScript is disabled by default, and you should not use addJavascriptInterface() unless you wrote all the HTML and JavaScript in the view. The Manage WebView objects page recommends keeping Google Safe Browsing on at all times, which the SDK already does. The trade-offs of every path are compared in Offerwall integration: iframe, link or API?

Step 5: credit coins only from postbacks

Events in the app can be faked on a rooted or modified device, so the app never reports a reward. Your server adds coins only when a verified postback arrives. Set your postback URL under Apps, your app, Postback, with the macros you need:

https://yoursite.com/postback?user={user_id}&tx={transaction_id}&event={event_id}&status={status}&reward={reward}&payout={payout}&hash={hash}
StatusStateYour server
1creditedAdd {reward}, once per transaction_id
2reversedTake {reward} back, only if you credited that transaction_id

Check the hash before anything else. It is the HMAC-SHA256, in lowercase hex, of the transaction ID, user ID, reward, and status joined with colons, signed with your postback secret key, which is a different value from the link hash salt:

const expected = crypto.createHmac('sha256', postbackSecretKey)
  .update(`${tx}:${userId}:${reward}:${status}`)
  .digest('hex');

Compare it in constant time, then store each transaction_id with the last status you processed, because a retry can deliver the same event twice. Answer with any 2xx status within 6 seconds and without a redirect. If your server fails, the postback is retried up to 5 times, after about 1 minute, 5 minutes, 30 minutes, 2 hours, and 6 hours. Full handlers in PHP, Node.js, and Python are in the Postbacks docs, and the checklist is in Postback security: verify every reward call.

A reversal is a new postback for the same transaction, with the same positive amounts. Sample values from the Credits and reversals docs:

Credit:   tx=8f3k2a  status=1  state=credited  reward=315  payout=0.315
Reversal: tx=8f3k2a  status=2  state=reversed  reward=315  payout=0.315

Decide early what happens when a user already spent coins that are later reversed, for example letting the balance go below zero or holding withdrawals, and put it in your terms. Clicks from the SDK arrive with sdk in the {integration} macro and show as Mobile SDK in your dashboard, so you can compare the app with your website. If you are new to postbacks, start with what a postback URL is.

Google Play policy, in general terms

Google Play allows rewarded experiences inside apps, but it has firm rules around incentives, and it changes them from time to time. This is general information, not legal advice. Points that matter most for an Android app with an offerwall (checked on October 21, 2026):

  • Never reward ratings or reviews. Google’s User Ratings, Reviews, and Installs policy says not to offer rewards or incentives for ratings or reviews, of your app or any other.
  • Installs are not your main feature. The same policy prohibits incentivizing users to install other apps as the app’s main functionality. The wall should be an optional extra inside a real app or game.
  • Apps for children. The Families Self-Certified Ads SDK Policy says offerwalls that child users cannot clearly identify as advertising are not allowed. End users of a Sharklio offerwall must be at least 16 anyway, with parental permission under 18. See offerwall COPPA rules.
  • Be clear about what users get. Show the reward and the task before the user starts, and keep the Earn button labeled as earning, not as a free gift.

Our guide to incentivized installs and Google Play policy goes through every relevant policy page.

Step 6: test before you release

  1. Create a test account in your app and note its user ID.
  2. Install a debug build, tap Earn, and check that your server returns a token, the wall opens, and your onOpened listener runs.
  3. Open an offer that leads to Google Play and one that leads to a website. The first should open the Play Store app, the second the browser.
  4. Press back inside the wall, rotate the phone, then close the wall. You should land back on your screen, and onClosed should reload the balance.
  5. In Integration status on the Integration tab, use Send a test postback for the test user’s ID with status 1. The balance should rise once. A test always has a transaction_id starting with test_ and offer_id 0, so your handler can tell it apart. A status 2 test is a new transaction you never credited, so it must change nothing.
  6. Build the minified release variant and repeat steps 2 to 4.

Do not test through a VPN or a cloud device

The wall blocks VPNs, proxies, Tor, and data center networks, and the check also runs inside the SDK. On a VPN, a remote desktop, or a test device in the cloud, you see a page such as VPN detected or Network not supported instead of offers. Test on a real phone on home Wi-Fi or mobile data.

Every postback attempt and your server’s answer is listed under Reports, Postbacks. The full routine is in How to test your app before it goes live, and the Testing docs list common errors with fixes.

Building with a cross-platform framework?

The same session flow powers the Flutter offerwall guide, the React Native offerwall guide, and the Unity offerwall guide, which all use the native Android SDK inside. For iPhone, see how to add an offerwall to an iOS app in Swift.

How Sharklio fits your Android app

Sharklio is a pay-per-result affiliate network, and the Android SDK is one of the ways publishers run its offerwall, next to the signed link, the Offers API, the iOS SDK, and the Flutter, React Native, and Unity plugins. Your server creates a single-use session, so no key or salt ships in your APK. Each app sets its own currency name and icon, conversion rate, user split, rounding, and wall design, offers match each user’s country and device, and every credit and reversal reaches your server as a signed postback. Reports break results down by country, offer, and sub ID. Every publisher application and every app is reviewed by hand before the wall goes live. See how the wall works, read how to get approved as a publisher, or get in touch.

Frequently asked questions

Is there an offerwall SDK for Android in Kotlin?

Yes. Sharklio’s Android SDK is com.sharklio:offerwall:1.1.0 from the Maven repository at https://sharklio.com/maven, and you call it from Kotlin with Sharklio.init and Sharklio.open(activity, token). It works from Java with the same calls.

What minSdk does the Android offerwall SDK need?

Android 5.0, which is API level 21, or newer. The SDK has no third-party dependencies and brings its own R8 and ProGuard rules.

Do I put my API key in the Android app?

No. The API key, the link hash salt, and the postback secret key stay on your server, because anyone can read an app’s files. The app holds only the App ID and a token your server created for the signed-in user.

Can the app add coins when onClosed fires?

No. onClosed only means the user left the wall, and app events can be faked. Add coins on your server from verified status 1 postbacks, then reload the balance in the app.

Why do I get TOKEN_EXPIRED_OR_USED?

A session token works once and for 10 minutes. Ask your server for a new token every time the user taps Earn instead of storing one.

Why does the wall show VPN detected on my test phone?

The wall blocks VPN, proxy, Tor, and data center connections, including in the SDK. Turn the VPN off or switch to mobile data, and do not test on cloud devices.

Can my website and Android app share one user balance?

Yes, if you use the same user ID for the same person on both. If they need different currencies, add a second app in your dashboard.