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.
| Part | Does | Never does |
|---|---|---|
| React Native app | Asks your server for a token, calls Sharklio.open, listens for events, reloads the balance | Holds the API key or link hash salt, adds currency by itself |
| Your server | Creates sessions with the API key, verifies postbacks, owns the wallet | Trusts the app about rewards |
| Sharklio | Shows offers for the user’s country and device, tracks results, sends signed postbacks | Pays 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 type | Supported | Extra setup |
|---|---|---|
| Bare React Native (Android) | Yes, uses the Android SDK inside | None, unless settings.gradle blocks project repositories |
| Bare React Native (iOS) | Yes, uses the iOS SDK inside | pod install in the ios folder |
Expo with a development build, npx expo prebuild or EAS Build | Yes | Add the config plugin to app.json |
| Expo Go | No | Expo Go cannot load native code |
| Web and other platforms | No | open 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.sharkliogroup) and the Android SDK to your Gradle build, the same way React Native adds its own repositories. If yoursettings.gradlesetsrepositoriesModetoFAIL_ON_PROJECT_REPOSorPREFER_SETTINGS, addmaven { 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 installin youriosfolder.
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:
| Field | Required | Notes |
|---|---|---|
user_id | Yes | The same user ID you receive in postbacks. 1 to 100 characters: letters, numbers, and . _ @ : + - |
sub1 to sub5 | No | Your 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.
| Event | When | What your app should do |
|---|---|---|
opened | The offerwall finished loading its first page | Stop any loading spinner on your button |
closed | The user closed the offerwall | Load the user’s balance from your server |
error | A SharklioError with a code | Create 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.
| Status | State | Your server |
|---|---|---|
| 1 | credited | Add {reward} to the user, once per {transaction_id} |
| 2 | reversed | Take {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 Connect | Google Play Data safety | Linked to the user | Used for tracking | Purpose |
|---|---|---|---|---|
| User ID | User IDs | Yes | No | App Functionality |
| Device ID | Device or other IDs | Yes | No | App Functionality |
| Coarse Location | Approximate location | Yes | No | App Functionality |
| Product Interaction | App interactions | Yes | No | App 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
- Create a test account in your app and note its user ID.
- 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.
- Check that the offerwall opens, that an offer opens in the store or the browser, and that the back and close buttons work.
- Confirm the
closedevent fires and your app reloads the balance. - Open the same token a second time. You should get
TOKEN_EXPIRED_OR_USED, which proves your app fetches a new token each time. - 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.