How to Add an Offerwall to a React Native App

Key takeaways

  • Your server calls POST /v1/session with the API key and the user's ID, and the app only ever sees a token that works once and for 10 minutes.
  • Bare React Native 0.77+ needs only the .npmrc scope line, the install, and pod install; Expo SDK 53+ adds the config plugin and runs in a development build, never in Expo Go.
  • The closed event is the moment to reload the balance, but only the signed postback on your server adds or takes back currency.

How do you add an offerwall to a React Native app?

To add an offerwall to a React Native app, install the @sharklio/react-native-offerwall package, have your server create a short, single-use session for the signed-in user, and call Sharklio.open(token) in the app. The package shows the offerwall in a native screen on Android and iOS. Rewards never travel through the app: Sharklio sends a signed postback to your server, and your server credits the user’s balance.

The same package works in bare React Native projects and in Expo apps that use a development build or EAS Build. This guide follows the official React Native SDK docs step by step, and adds the decisions you make around them. Where to place the wall and how to price your currency are covered in Monetize a mobile game with an offerwall.

Who does what

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

PartDoesNever does
React Native appAsks your server for a token, calls Sharklio.open, listens for events, reloads the balanceHolds the API key or link hash salt, adds currency by itself
Your serverCreates sessions with the API key, verifies postbacks, owns the walletTrusts the app about rewards
SharklioShows offers for the user’s country and device, tracks results, sends signed postbacksPays your users directly

React Native’s own Security page says: “Never store sensitive API keys in your app code,” because anything in your code “could be accessed in plain text by anyone inspecting the app bundle.” The session flow is the “orchestration layer between your app and the resource” that page recommends.

Before you start: versions and setup paths

Check these requirements from the Sharklio docs before you install anything:

  • React Native 0.77 or newer, or Expo SDK 53 or newer.
  • The package works with the New Architecture and, on React Native 0.77 to 0.81, with the old architecture too.
  • Your Android project needs Kotlin 2.0 or newer, which these React Native versions use by default.
  • iOS builds need Xcode 16 or newer.
  • An approved publisher account and an app. The App ID and the API key are in your dashboard under Apps, your app, Integration.

Your setup path depends on how the project is built:

Project typeSupportedExtra setup
Bare React Native (Android)Yes, uses the Android SDK insideNone, unless settings.gradle blocks project repositories
Bare React Native (iOS)Yes, uses the iOS SDK insidepod install in the ios folder
Expo with a development build, npx expo prebuild or EAS BuildYesAdd the config plugin to app.json
Expo GoNoExpo Go cannot load native code
Web and other platformsNoopen reports UNSUPPORTED

Still choosing between a package, a plain link, or your own wall? See Offerwall integration: iframe, link or API? and Build your own offerwall with an Offers API.

Step 1: install the package

The package is in the Sharklio npm registry, not on the public npm registry. Point the @sharklio scope at it by adding this line to the .npmrc file next to your package.json:

@sharklio:registry=https://sharklio.com/npm/

This is the standard npm way to map a scope to a registry. The npm docs on scopes explain that once a scope is associated with a registry, any install of a package with that scope requests it from that registry instead. Then install the package and rebuild your app:

npm install @sharklio/react-native-offerwall
# or
yarn add @sharklio/react-native-offerwall
# Expo
npx expo install @sharklio/react-native-offerwall

npm, Yarn 1 and pnpm read .npmrc. With Yarn 2 or newer, put the scope in .yarnrc.yml instead, using Yarn’s npmScopes setting:

npmScopes:
  sharklio:
    npmRegistryServer: "https://sharklio.com/npm"

Prefer a file? The docs page also offers sharklio-react-native-offerwall-1.0.0.tgz; install it with npm install ./sharklio-react-native-offerwall-1.0.0.tgz.

Bare React Native: Android and iOS

There is nothing else to wire up by hand in most projects:

  • Android: the package adds the Sharklio Maven repository (only for the com.sharklio group) and the Android SDK to your Gradle build, the same way React Native adds its own repositories. If your settings.gradle sets repositoriesMode to FAIL_ON_PROJECT_REPOS or PREFER_SETTINGS, add maven { url = uri("https://sharklio.com/maven") } to the repositories there.
  • iOS: the package brings the Sharklio iOS SDK with it, and CocoaPods links and embeds it. Run pod install in your ios folder.

Expo: config plugin and development build

Add the config plugin to app.json, then run npx expo prebuild or start an EAS Build:

{
  "expo": {
    "plugins": ["@sharklio/react-native-offerwall"]
  }
}

Expo’s prebuild docs describe config plugins as functions that modify the native projects during prebuild, which is why the plugin line is all an Expo app needs. Expo’s FAQ also says “Expo Go cannot use third-party libraries that require custom native code.” So in Expo Go, Sharklio.isSupported is false and open reports UNSUPPORTED. Test in a development build, which Expo describes as your own version of Expo Go that can use any native library.

Do not debug the wall in Expo Go

If the Earn button does nothing in Expo Go, nothing is broken: the native screen is not available there. Make one development build and keep your fast reload loop on top of it.

Step 2: create a session on your server

When the user taps Earn, the app asks your own server for a token. Your server calls POST https://api.sharklio.com/v1/session with the API key in the Authorization header and the user’s ID, as a form or as JSON:

FieldRequiredNotes
user_idYesThe same user ID you receive in postbacks. 1 to 100 characters: letters, numbers, and . _ @ : + -
sub1 to sub5NoYour own tracking values, returned in the postback as {sub1} to {sub5}

Here is a Node.js endpoint behind your normal login, built on the request from the docs. It tags the session with where the user opened the wall, so you can compare placements later:

app.post('/earn/session', requireAuth, async (req, res) => {
  const placement = ['shop', 'home'].includes(req.body.placement) ? req.body.placement : 'other';
  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: String(req.user.id), sub1: placement }),
  });
  if (r.status === 403) return res.status(403).json({ error: 'offerwall_unavailable' });
  if (!r.ok) return res.status(502).json({ error: 'session_failed' });
  const { token } = await r.json();
  res.json({ token });
});

requireAuth stands for your own login middleware, and the route assumes a JSON body parser such as express.json(). Three rules matter more than the code:

  • Use your account ID as the user ID. Not a device ID, which changes on reinstall, and not an email address. Use the same ID if the user also earns on your website.
  • Only return tokens for the signed-in user. The user ID comes from your session, never from the request body.
  • One token per opening. A token works once and for 10 minutes, so create a new one every time the user taps Earn.

The errors are short and specific: 400 invalid_user_id, 401 invalid_api_key, 403 app_not_live or user_blocked (do not show the offerwall), 405 method_not_allowed, and 429 rate_limited (retry after the time in the Retry-After header). Each app can make up to 600 requests per minute. More on sub IDs is in What are click IDs and sub IDs?

Step 3: open the wall and listen for events

Call init once with the App ID from the Integration tab. Then open the offerwall with a fresh token. A complete Earn button component:

import { useEffect } from 'react';
import { Button } from 'react-native';
import { Sharklio } from '@sharklio/react-native-offerwall';

Sharklio.init('YOUR_APP_ID');

export function EarnButton({ authToken, refreshBalance }) {
  useEffect(() => {
    const closed = Sharklio.addListener('closed', refreshBalance);
    const failed = Sharklio.addListener('error', (error) => {
      console.warn(`${error.code}: ${error.message}`);
    });
    return () => {
      closed.remove();
      failed.remove();
    };
  }, [refreshBalance]);

  async function openOfferwall() {
    const res = await fetch('https://api.yourapp.example/earn/session', {
      method: 'POST',
      headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ placement: 'shop' }),
    });
    if (!res.ok) return;
    const { token } = await res.json();
    Sharklio.open(token);
  }

  if (!Sharklio.isSupported) return null;
  return <Button title="Earn coins" onPress={openOfferwall} />;
}

open never throws for offerwall problems. They arrive as events, and errors from init and open arrive right after the call returns, so a listener you add in the same tick still receives them. TypeScript types are included.

EventWhenWhat your app should do
openedThe offerwall finished loading its first pageStop any loading spinner on your button
closedThe user closed the offerwallLoad the user’s balance from your server
errorA SharklioError with a codeCreate a new session and try again, or hide the button

The error codes are NOT_INITIALIZED, INVALID_APP_ID, INVALID_TOKEN, TOKEN_EXPIRED_OR_USED, NETWORK, UNSUPPORTED, and UNKNOWN, the same names the native Android and iOS SDKs use. In practice, TOKEN_EXPIRED_OR_USED and NETWORK deserve a “Try again” button that fetches a new token, while NOT_INITIALIZED and INVALID_APP_ID are setup bugs to fix before release.

Many offers send the user to a store or a browser, and some credit later. To also refresh when the user returns to your app, React Native’s AppState API reports a change to active when the app returns to the foreground.

Match the top bar to your app

The top bar shows the Sharklio mark with your title and colors from the Design tab. To set them in code, or to hide the mark, pass options. The color can be any color string React Native understands; transparency is ignored.

Sharklio.open(token, {
  title: 'Earn coins',
  toolbarColor: '#004ABB',
  showLogo: false,
});

Step 4: credit rewards only on your server

The closed event tells you the user left the wall. It does not tell you what they earned, and events can be faked on a user’s device. Rewards reach your server through the same postback as every other Sharklio integration: a GET request to the URL you save under Apps, your app, Postback.

StatusStateYour server
1creditedAdd {reward} to the user, once per {transaction_id}
2reversedTake {reward} back, only if you credited that transaction

Before you touch a balance, verify {hash}: an HMAC-SHA256, in lowercase hex, of the transaction ID, user ID, reward, and status joined with colons, signed with your postback secret key. That key is a different value from the link hash salt. Store the last status per transaction ID and skip repeats, answer with a 2xx status within 6 seconds, and do not redirect. Failed postbacks are retried up to 5 times. The full handler, in PHP, Node.js and Python, is in the Postbacks docs, and the reasoning behind each check is in Postback security: verify every reward call. If postbacks are new to you, start with What is a postback URL?

Clicks through the package show as sdk in the {integration} postback macro and as Mobile SDK in your dashboard. Add {sub1} to your postback URL to see which placement earns the most.

Illustrative example: reload, do not guess

A user with 1,000 coins finishes a 450-coin offer and closes the wall. Your closed listener asks your server: if the postback has arrived, it says 1,450; if the offer credits after a review, it still says 1,000. The app never adds 450 itself. Sample figures only.

Store policies and privacy forms, in general terms

Apple and Google both have rules about rewarded features, and both update them, so read the current versions before you submit a build with an offerwall. The points that most often matter:

  • Keep the wall optional. Apple’s App Review Guidelines, guideline 3.2.2(x), say apps “must not force users to rate the app, review the app, download other apps, or other store-related actions” to use the app, while they “may otherwise incentivize users to take specific actions within apps.”
  • Never reward ratings or reviews, of your app or any other. Google Play forbids incentivized ratings and reviews as well.
  • Make sure the app has its own real purpose. An app whose main function is rewarding installs of other apps is a problem on both stores.
  • Keep it out of apps for children. End users of a Sharklio offerwall must be at least 16, with parental permission under 18.

For detail, read Can you put an offerwall in an iOS app? and Incentivized installs: Google Play policy. Policy facts here were checked on October 21, 2026.

Both stores also ask you to declare data that third-party code in your app collects. Apple’s App privacy details page includes “the practices of third-party partners whose code you integrate into your app,” and Google’s Data safety help page says you must reflect data collection by third-party code in the form. The iOS SDK inside the package has a privacy manifest, and the docs list what to add:

App Store ConnectGoogle Play Data safetyLinked to the userUsed for trackingPurpose
User IDUser IDsYesNoApp Functionality
Device IDDevice or other IDsYesNoApp Functionality
Coarse LocationApproximate locationYesNoApp Functionality
Product InteractionApp interactionsYesNoApp Functionality

The device ID is used for fraud prevention and the coarse location comes from the IP address. The SDK does not track users across apps and does not use the advertising identifier.

Step 5: test before you ship

  1. Create a test account in your app and note its user ID.
  2. Create a session from your server for that ID and open it in a development build on an Android phone, an iPhone, or the iOS Simulator.
  3. Check that the offerwall opens, that an offer opens in the store or the browser, and that the back and close buttons work.
  4. Confirm the closed event fires and your app reloads the balance.
  5. Open the same token a second time. You should get TOKEN_EXPIRED_OR_USED, which proves your app fetches a new token each time.
  6. In Integration status on the Integration tab, use Send a test postback for the test user with status 1. The balance should go up once. A status 2 test is a new transaction you never credited, so it must change nothing. Test transaction IDs start with test_.

Test on a normal home or mobile connection. On a VPN, a proxy, or a test device in the cloud, the offerwall shows a block page such as VPN detected instead of offers. Every postback attempt and your server’s answer is listed under Reports, Postbacks. The full checklist is in How to test your app before it goes live, and the common errors are in the Testing and troubleshooting docs.

Building for other stacks too? The same session flow powers the Flutter offerwall guide, the Android offerwall in Kotlin, the iOS offerwall in Swift, and the Unity offerwall guide.

Sharklio in your React Native app

Sharklio is a pay-per-result affiliate network: your users earn your own currency by completing offers, and you earn for each completed offer. The @sharklio/react-native-offerwall package opens the wall in a native screen with a single-use session token from your server, so no key ships in your JavaScript bundle. Each app sets its own currency name and icon, conversion rate, user split, rounding, and wall design with a live preview, and a signed postback reaches your server for every credit and every reversal. Offers depend on the user’s country and device, and VPN, proxy, Tor and data center traffic is blocked. Every publisher application and every app is reviewed by hand before the wall goes live. See how the wall works or read how to get approved as a publisher.

Frequently asked questions

Does the Sharklio offerwall work with Expo?

Yes, with Expo SDK 53 or newer in a development build, npx expo prebuild or EAS Build, after you add the config plugin to app.json. It does not work in Expo Go, which cannot load native code.

Which React Native versions does the offerwall package support?

React Native 0.77 or newer, on the New Architecture, and on the old architecture too for 0.77 to 0.81. Android needs Kotlin 2.0 or newer, and iOS builds need Xcode 16 or newer.

Why can’t I put the API key in my React Native app?

Anyone can read an app’s files, including its JavaScript bundle, and could use the key to open the wall as any user. Keep the key on your server and send the app only a token that works once and for 10 minutes.

Can I credit the reward when the closed event fires?

No. Events can be faked on a user’s device, and many offers credit later. Use closed to reload the balance, and credit only from the verified postback on your server.

Why does the offerwall show “VPN detected” on my test device?

The wall blocks VPNs, proxies, Tor, and data center networks, which includes cloud test devices. Test on a real phone on home Wi-Fi or mobile data.

Does the package work on React Native for web?

No. On web and other platforms, open checks the app ID and the token and then reports UNSUPPORTED. For a website, use the offerwall link or iframe instead.