Key takeaways
- Sharklio has no Capacitor plugin: your backend builds the signed offerwall link, and the app opens it with Browser.open from @capacitor/browser (Custom Tabs on Android, SFSafariViewController on iOS).
- Do not put the offerwall in an iframe inside the app or navigate the main Capacitor WebView to it: the wall only loads in frames on your registered https website, and allowNavigation is not meant for production.
- Only your server credits currency, from verified postbacks; the app reloads the balance on browserFinished and on the App resume event.
How do you add an offerwall to an Ionic Capacitor app?
To add an offerwall to an Ionic Capacitor app, have your backend build a signed offerwall link for the signed-in user, open that link from the app with the official @capacitor/browser plugin (or @capacitor/inappbrowser), and credit the user on your server when the offerwall sends a postback. The app never holds a secret and never decides that a reward was earned. When the user closes the browser or comes back to the app, it reloads the balance from your server.
Sharklio has no Capacitor plugin. Its SDKs cover native Android, native iOS, Flutter, React Native and Unity. For Ionic and Capacitor, the real path is the signed link, which the Offerwall link docs say can be opened directly in a tab, a new window or a mobile WebView from anywhere. For the SDK route in another cross-platform framework, compare the Flutter guide.
Who does what
| Part | Does | Never does |
|---|---|---|
| Ionic app (TypeScript) | Asks your server for the link, opens it in a browser view, reloads the balance | Holds the link hash salt, builds the hash, adds coins by itself |
| Your backend | Signs in the user, builds the signed link, receives 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 |
The user taps Earn, your backend returns a fresh signed link, the app opens it, and a postback later tells your backend what to credit. New to postbacks? What is a postback URL? explains the idea.
Which way to open the offerwall in Capacitor?
A Capacitor app is a web app inside a native WebView, so it is tempting to show the wall inside your own pages. That is the one thing that does not work:
| Option | What the user sees | Store links | Fits? |
|---|---|---|---|
@capacitor/browser: Browser.open | Chrome Custom Tabs on Android, SFSafariViewController on iOS | Handled by the browser | Yes, the default choice |
@capacitor/inappbrowser: openInSystemBrowser | The same browser views, with more options | Handled by the browser | Yes |
@capacitor/inappbrowser: openInWebView | A native web view with its own toolbar and close button | Partly handled; test both platforms | Yes, with testing |
| iframe inside your app’s pages | Nothing: the wall refuses to load | Not applicable | No |
| Main Capacitor WebView navigated to the wall | Your app replaced by the wall | Yours to handle | No |
| Your own plugin that calls the native SDKs | A native offerwall screen | Handled by the SDK | Advanced, unofficial |
Why the two “No” rows. The Offerwall link docs say the wall only loads inside frames and pop-ups on the website you registered for the app and its subdomains, and only on https pages. Your app’s own pages are not that website: with the default settings in the Capacitor configuration docs, they run on https://localhost on Android and capacitor://localhost on iOS. The same page says that, by default, external URLs open in the external browser and not in the WebView, and that server.allowNavigation, which changes this, is not intended for production. The broader trade-offs between a link, an iframe and an API are in Offerwall integration: iframe, link or API?
Capacitor shortcuts that break the offerwall
- Building the hash in TypeScript. Everything in your web bundle ships inside the app, so the salt would be readable by anyone who unpacks it.
- An
<iframe>of the wall in an Ionic page. It stays blank in the native app. - Adding
wall.sharklio.comtoallowNavigationso the wall replaces your app. You lose your UI, the back path and store link handling.
Step 1: return the signed link from your backend
The link format, from the Offerwall link docs, is https://wall.sharklio.com/{APP_ID}?user_id={USER_ID}&hash={HASH}. The hash is an HMAC-SHA256 of the user ID with your link hash salt, in lowercase hex. Your App ID and salt are in your dashboard under Apps, your app, Integration. A Node.js endpoint behind your normal sign-in:
const crypto = require('crypto');
app.get('/offerwall-link', requireAuth, (req, res) => {
const userId = String(req.user.id);
const hash = crypto.createHmac('sha256', process.env.SHARKLIO_LINK_SALT).update(userId).digest('hex');
res.json({
url: `https://wall.sharklio.com/${process.env.SHARKLIO_APP_ID}?user_id=${encodeURIComponent(userId)}&hash=${hash}&sub1=capacitor_app`,
});
});
The same hash in PHP, as the docs show it:
$hash = hash_hmac('sha256', $userId, $linkHashSalt);
$url = 'https://wall.sharklio.com/' . $appId . '?user_id=' . rawurlencode($userId) . '&hash=' . $hash;
requireAuthstands for your own authentication middleware. Take the user ID from your server session, never from the request, and use your database ID, not an email or a device ID. It may be 1 to 100 characters fromA-Z a-z 0-9 . _ @ : + -.sub1is optional and not part of the hash. It comes back in every postback as{sub1}, so you can tell app traffic from your website if you run both on one Sharklio app.- The hash depends only on the user ID, so treat the link as private to that user. If one leaks, see how to rotate your keys and link salt.
- Your Ionic app calls this endpoint with
fetch. If your API checks CORS, allow the app’s origins (https://localhostandcapacitor://localhostby default).
Step 2: open the link with @capacitor/browser
Install the official plugin and sync the native projects:
npm install @capacitor/browser
npx cap sync
The Capacitor Browser API docs say the plugin uses SFSafariViewController on iOS, and it is built on androidx.browser (Chrome Custom Tabs) on Android. Its open method takes a url, a toolbarColor, and, on iOS, a presentationStyle of fullscreen or popover. At the time of writing the current releases are Capacitor 8.6 and @capacitor/browser 8.0.5 (checked on npm on October 29, 2026).
This plain TypeScript works in Ionic Angular, React or Vue. Call openOfferwall from your Earn button:
import { Capacitor } from '@capacitor/core';
import { Browser } from '@capacitor/browser';
const WALL_PREFIX = 'https://wall.sharklio.com/';
async function fetchOfferwallLink(authToken: string): Promise<string> {
const res = await fetch('https://api.your-app.example/offerwall-link', {
headers: { Authorization: `Bearer ${authToken}` },
});
if (!res.ok) {
throw new Error(`Offerwall link request failed: ${res.status}`);
}
const { url } = (await res.json()) as { url: string };
if (!url.startsWith(WALL_PREFIX)) {
throw new Error('Unexpected offerwall URL');
}
return url;
}
export async function openOfferwall(authToken: string): Promise<void> {
const url = await fetchOfferwallLink(authToken);
if (Capacitor.isNativePlatform()) {
await Browser.open({ url, toolbarColor: '#004ABB' });
} else {
window.location.assign(url);
}
}
- Fetch a fresh link on every tap and never store it. The prefix check makes sure the app only opens the offerwall host.
- On the web build,
Browser.opencallswindow.open, which pop-up blockers often stop after anawait. The code opens the wall in the same tab there instead. If your web build runs on the website you registered for the app, an iframe or the floating Earn button also works on that domain.
We type-checked these samples with TypeScript in strict mode against @capacitor/core 8.6.0, @capacitor/browser 8.0.5 and @capacitor/app 8.1.2 on October 29, 2026.
Want your own toolbar? @capacitor/inappbrowser
The newer official InAppBrowser plugin (4.1.0 at the time of writing) has three methods: openInExternalBrowser, openInSystemBrowser (the same Custom Tabs and SFSafariViewController views) and openInWebView, a native web view with a close button. Its docs say it needs Android API 26 as the minimum SDK, which is higher than a new Capacitor app’s default, and its iOS package targets iOS 15.
import { InAppBrowser, DefaultWebViewOptions } from '@capacitor/inappbrowser';
export async function openOfferwallInWebView(url: string, refreshBalance: () => Promise<void>): Promise<void> {
await InAppBrowser.addListener('browserClosed', () => {
void refreshBalance();
});
await InAppBrowser.openInWebView({
url,
options: {
...DefaultWebViewOptions,
showURL: false,
closeButtonText: 'Done',
android: { ...DefaultWebViewOptions.android, hardwareBack: true },
iOS: { ...DefaultWebViewOptions.iOS, allowsBackForwardNavigationGestures: true },
},
});
}
One limit to know: the plugin has no option to decide which links stay inside the web view. From JavaScript you can only watch navigations with the browserPageNavigationCompleted event. In the Android library it uses (OSInAppBrowserLib-Android 2.1.0), Play Store and intent: links are sent to the system and every other web link loads inside the web view. We did not inspect the iOS side, so try App Store offers on an iPhone before you ship. The full WebView checklist is in How to open an offerwall in a WebView safely.
Step 3: refresh the balance when the user returns
Rewards never travel through the browser into your app. Reload the balance from your backend at two moments:
import { App } from '@capacitor/app';
import { Browser } from '@capacitor/browser';
export async function watchOfferwallReturn(refreshBalance: () => Promise<void>): Promise<void> {
await Browser.addListener('browserFinished', () => {
void refreshBalance();
});
await App.addListener('resume', () => {
void refreshBalance();
});
}
browserFinishedfires when the user closes the browser view (Android and iOS only, per the Browser API docs).- The App API
resumeevent covers users who go from an offer to the store or the advertiser’s app and then back to yours. Install it withnpm install @capacitor/app.
Register the listeners once, when your Earn page loads. Many offers credit only after a later step or a review, so a reward is often not there on the first return; a reward history in your UI, filled from credited postbacks, helps users see what arrived.
Step 4: credit users on your backend, from postbacks
An event in the app can be faked, so browserFinished is a reason to refresh, never a reason to add currency. Your backend credits the user only when a verified postback arrives. Add your postback URL under Apps, your app, Postback, with the macros you need:
https://yoursite.com/postback?user={user_id}&tx={transaction_id}&status={status}&reward={reward}&hash={hash}
| Status | Meaning | Your backend |
|---|---|---|
| 1 | Credited | Add {reward}, once per transaction ID |
| 2 | Reversed | Take it back, only if you credited that transaction |
Verify {hash} first: an HMAC-SHA256, in lowercase hex, of the transaction ID, user ID, reward and status joined with colons, signed with your postback secret key (a different value from the link hash salt). Store the last status per transaction ID and skip repeats, and answer with a 2xx status within 6 seconds, with no redirect. Clicks from a browser view show as offerwall in the {integration} macro. The full reference is in the Postbacks docs.
Ready-made handlers: Node.js, Firebase Cloud Functions, Supabase, Python and PHP and Laravel. See also Postback security and preventing duplicate reward credits.
Advanced: call the native Android or iOS SDK from a custom plugin
A Capacitor app ships real Android and Xcode projects, so you can add Sharklio’s native SDKs there and call them through a plugin you write. There is no official Capacitor bridge for this. You build it and you maintain it through Capacitor upgrades. Most apps do not need it: the link above already works.
If you want a native offerwall screen anyway, the shape is:
- Server: instead of a signed link, your backend calls
POST https://api.sharklio.com/v1/sessionwith your API key and the user ID, and returns only thetoken. A token works once and for 10 minutes, so create one per tap. - Android: add the Sharklio Maven repository and
com.sharklio:offerwall:1.1.0to theandroidproject, callSharklio.init(context, "YOUR_APP_ID")once, and in your plugin method callSharklio.open(activity, token)with the plugin’s activity. Capacitor’s Android custom code guide shows how a@CapacitorPluginclass is registered inMainActivity. - iOS: add
SharklioOfferwallwith Swift Package Manager or CocoaPods, callSharklio.configure(appId: "YOUR_APP_ID"), and callSharklio.open(token: token)from your plugin. The iOS custom code guide shows aCAPPluginregistered incapacitorDidLoad(). - Events: forward the SDK’s close event (
SharklioListeneron Android,Sharklio.onClosedon iOS) to JavaScript withnotifyListeners, and refresh the balance there.
On the TypeScript side, the plugin is a typed registerPlugin call. SharklioBridge is a name you choose, not a package:
import { registerPlugin } from '@capacitor/core';
import type { PluginListenerHandle } from '@capacitor/core';
export interface SharklioBridgePlugin {
open(options: { token: string }): Promise<void>;
addListener(eventName: 'closed', listenerFunc: () => void): Promise<PluginListenerHandle>;
}
export const SharklioBridge = registerPlugin<SharklioBridgePlugin>('SharklioBridge');
Every class name and method on the native side must match the Android SDK docs and iOS SDK docs. Postbacks stay exactly as in Step 4, and clicks show as sdk instead of offerwall. The native guides are Android with Kotlin and iOS with Swift.
Store policies, in general terms
A Capacitor app gets the same review as any native app. Read the current rules before you submit (all checked on October 29, 2026):
- Apple’s App Review Guidelines 3.2.2 say apps must not force users to rate the app, review the app, download other apps, or take similar store actions to access functionality, and that apps may otherwise reward specific actions. Keep the wall optional and never a gate.
- Guideline 4.2 asks for features and UI that lift an app beyond a repackaged website; an offerwall does not fix a thin web wrapper.
- Google Play’s User Ratings, Reviews, and Installs policy names incentivized reviews and ratings, and incentivizing installs of other apps as an app’s main functionality, as manipulation. Never reward ratings or reviews, of your app or any other.
- Google Play’s Spam policy rules out apps whose primary purpose is to drive affiliate traffic to a website. Your app needs its own reason to exist.
End users of a Sharklio offerwall must be at least 16, with parental permission under 18, so keep it out of apps for children (offerwall COPPA rules). Users on VPNs, proxies, Tor or data center networks are blocked from the wall, so it is the wrong fit for a VPN or proxy app. The details per store are in Can you put an offerwall in an iOS app? and Incentivized installs: Google Play policy. Also cover the offerwall in your App Store privacy details and Google Play Data safety answers, as the Sharklio testing checklist asks.
Step 5: test on real phones
- In Integration status on your app’s Integration tab, use Check a link, then Test a link with your test user’s ID. Compare the result with what your endpoint returns. They must match exactly. Open as a test user shows the wall without any code, as
owner-test. - Run
npx cap run androidandnpx cap run ioson real devices, on a normal home connection or mobile data. A VPN, a proxy or a cloud device farm shows a block page such as VPN detected instead of offers. - Tap Earn. The wall should open in Custom Tabs or SFSafariViewController, not inside your app’s pages.
- Start an offer that leads to a store. It should open the Play Store or App Store app. Come back and check that the balance reloads.
- Use Send a test postback with status 1 for the test user. The balance should go up once. Test transaction IDs start with
test_and haveoffer_id0, so a status 2 test must change nothing. - Check Reports, Postbacks for every attempt and the status code your server returned, then remove the test rewards.
Running ionic serve in a desktop browser?
That tests the web build only: native events such as browserFinished never fire there. Do the final pass on Android and iOS devices.
Common errors (Link not valid, Missing user ID, postbacks that time out) are listed in the Testing and troubleshooting docs, and How to test your app before it goes live walks through the full checklist.
A Sharklio offerwall in your Capacitor app
Sharklio is a pay-per-result affiliate network: your users complete offers for your own currency, with your own name, icon, rate and wall design, and every credit and reversal reaches your backend as a signed postback. Publisher applications are reviewed by hand, and once your account is approved, every app you add is live right away. See how the wall works, or get in touch with questions.
Building with other web stacks too? See Vue and Nuxt, Next.js and React, Electron and progressive web apps.
Frequently asked questions
Is there a Capacitor plugin for the Sharklio offerwall?
No. Sharklio has SDKs for native Android, native iOS, Flutter, React Native and Unity. In a Capacitor app, your backend signs the offerwall link and the app opens it with @capacitor/browser or @capacitor/inappbrowser.
Can I show an offerwall in an iframe in my Ionic app?
Not in the native app. The Sharklio wall only loads in frames on https pages of the website you registered for the app, and a Capacitor app runs on localhost. An iframe only works in a web build hosted on that website.
Should I use @capacitor/browser or @capacitor/inappbrowser?
Start with @capacitor/browser: it needs the least code, and Custom Tabs and SFSafariViewController handle store links themselves. Use openInWebView from @capacitor/inappbrowser only if you want its native toolbar, and test store links on both platforms.
How does my Capacitor app know a user earned a reward?
It does not find out from the browser. Sharklio sends a signed postback to your backend, your backend credits the user, and the app reloads the balance on browserFinished or the App resume event.
Can I call the native Android or iOS SDK from Capacitor?
Yes, through a custom plugin you write in the app’s native projects, using single-use session tokens from POST /v1/session. It is unofficial and advanced; the signed link is enough for most apps.



