This guide covers flutter offerwall SDK for mobile developers and publishers — with practical, technical detail you can apply today.
Offerwall SDK Integration: Flutter Complete Tutorial
The Flutter Monetization Gap
Flutter has over 1 million published apps and a developer community that grows every quarter. Yet most rewarded monetization platforms — ironSource, Tapjoy, Digital Turbine, AppLovin — do not offer a native Flutter SDK.
Flutter developers who want to add an offerwall are typically told to use a web-based offerwall (which means a compromised user experience) or to write their own platform channel wrappers around Android and iOS SDKs (which means significant maintenance overhead).
This tutorial walks through the complete integration of a rewarded offerwall SDK into a Flutter app. We use Perkox as the example platform because it’s the only rewarded monetization platform that supports Flutter natively, but the architectural patterns apply to any offerwall integration.
What you’ll build: A Flutter app that presents a rewarded offerwall to users, handles reward callbacks, and routes final reward validation through a server-side postback endpoint.
Architecture Overview
A Flutter offerwall integration works through platform channels — the bridge between Dart and native code.
┌─────────────────────────────────┐
│ FLUTTER APP (Dart) │
│ │
│ UI Layer │
│ ├── "Earn Rewards" button │
│ ├── Reward notification widget │
│ │
│ PerkoxService (Dart) │
│ ├── showOfferwall() │
│ ├── onReward callback │
│ └── onClose callback │
└──────┬──────────────────────────┘
│ MethodChannel
│ ("com.perkox.offerwall")
│
┌──┴──┐
│ │
▼ ▼
┌──────┐ ┌──────┐
│ANDROID│ │ iOS │
│(Kotlin)│ │(Swift)│
└──┬───┘ └──┬───┘
│ │
▼ ▼
┌─────────────────────────────┐
│ PERKOX NATIVE SDK │
│ Android: PerkoxOfferwall.aar │
│ iOS: PerkoxOfferwall.xcfwk │
└──────┬──────────────────────┘
│
▼
┌─────────────────────────────┐
│ OFFERWALL UI │
│ (presented natively) │
└──────────────────────────────┘
│
▼ (user completes offer)
┌─────────────────────────────┐
│ PERKOX TRACKING ENGINE │
│ Click → Conversion → Postback │
└──────┬──────────────────────┘
│
▼
┌─────────────────────────────┐
│ YOUR BACKEND SERVER │
│ Receives postback │
│ Validates {player_id} │
│ Credits user │
└──────────────────────────────┘
The offerwall itself is presented natively (not in a WebView), which means full native performance and user experience. The Dart side handles the UI trigger and receives reward callbacks through the platform channel.
Prerequisites
Before starting, you need:
- A Perkox publisher account. Sign up at pub.perkox.com.
- Your App ID and SDK Key. Available in the Perkox Publisher Dashboard.
- Flutter 3.10+ with Dart 3.0+
- Android setup: minSdk 21+, Kotlin 1.9+, Java 17
- iOS setup: iOS 13.0+, Swift 5.7+, Xcode 14+
Step 1: Create the Dart Service Layer
First, create a Dart service that communicates with the native SDKs through a MethodChannel.
// lib/services/perkox_service.dart
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
class PerkoxService {
static const MethodChannel _channel =
MethodChannel('com.perkox.offerwall');
final ValueChanged<Map<String, dynamic>>? onReward;
final VoidCallback? onClose;
PerkoxService({this.onReward, this.onClose});
void init() {
_channel.setMethodCallHandler(_handleMethodCall);
}
Future<void> showOfferwall({
required String appId,
required String sdkKey,
required String playerId,
}) async {
try {
await _channel.invokeMethod('showOfferwall', {
'appId': appId,
'sdkKey': sdkKey,
'playerId': playerId,
});
} on PlatformException catch (e) {
debugPrint("Perkox error: ${e.message}");
rethrow;
}
}
Future<dynamic> _handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'onReward':
final Map<String, dynamic> reward =
Map<String, dynamic>.from(call.arguments);
onReward?.call(reward);
break;
case 'onClose':
onClose?.call();
break;
}
}
void dispose() {
_channel.setMethodCallHandler(null);
}
}
Key design decisions:
- The
MethodChannelnamecom.perkox.offerwallmust match on all three platforms (Dart, Android, iOS). - Reward callbacks come from native → Dart. The native SDK fires
onReward, which sends data through the channel. - The
playerIdis passed from Dart to native — this is how your backend identifies which user to credit.
Step 2: Android Integration (Kotlin)
2.1 Add the SDK to Your Android Project
Download the Perkox Android SDK (.aar file) and place it in your android/app/libs/ folder.
your_flutter_app/
└── android/
└── app/
└── libs/
└── perkox-android-sdk-release.aar
Add the dependency to your android/app/build.gradle:
dependencies {
implementation files('libs/perkox-android-sdk-release.aar')
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.1'
}
2.2 Create the Android MethodChannel Handler
// android/app/src/main/kotlin/com/yourapp/MainActivity.kt
package com.yourapp
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import com.perkoxofferwall.sdk.PerkoxOfferwall
import com.perkoxofferwall.sdk.Offerwall
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.perkox.offerwall"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"showOfferwall" -> {
val appId = call.argument<String>("appId") ?: ""
val sdkKey = call.argument<String>("sdkKey") ?: ""
val playerId = call.argument<String>("playerId") ?: ""
showPerkoxOfferwall(appId, sdkKey, playerId, flutterEngine)
result.success(null)
}
else -> result.notImplemented()
}
}
}
private fun showPerkoxOfferwall(
appId: String, sdkKey: String, playerId: String,
flutterEngine: FlutterEngine
) {
val offerwall = PerkoxOfferwall.create(appId, sdkKey, playerId)
offerwall.onReward = { reward ->
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.invokeMethod("onReward", reward)
}
offerwall.onClose = {
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.invokeMethod("onClose", null)
}
offerwall.launch(this)
}
}
Step 3: iOS Integration (Swift)
3.1 Add the SDK to Your iOS Project
Download the Perkox iOS SDK. In Xcode: File → Add Package Dependencies → Add Local → select the extracted SDK folder.
3.2 Create the iOS MethodChannel Handler
// ios/Runner/AppDelegate.swift
import UIKit
import Flutter
import PerkoxOfferwall
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as? FlutterViewController
let channel = FlutterMethodChannel(
name: "com.perkox.offerwall",
binaryMessenger: controller!.binaryMessenger
)
channel.setMethodCallHandler { [weak self] call, result in
switch call.method {
case "showOfferwall":
guard let args = call.arguments as? [String: String],
let appId = args["appId"],
let sdkKey = args["sdkKey"],
let playerId = args["playerId"] else {
result(FlutterError(code: "invalid_args",
message: "Missing required arguments",
details: nil))
return
}
self?.showPerkoxOfferwall(appId: appId, sdkKey: sdkKey,
playerId: playerId, channel: channel)
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func showPerkoxOfferwall(appId: String, sdkKey: String,
playerId: String, channel: FlutterMethodChannel) {
let offerwall = PerkoxOfferwall.create(
appId: appId, sdkKey: sdkKey, playerId: playerId
)
offerwall.onReward = { reward in
DispatchQueue.main.async {
channel.invokeMethod("onReward", arguments: reward)
}
}
offerwall.onClose = {
DispatchQueue.main.async {
channel.invokeMethod("onClose", arguments: nil)
}
}
if let rootVC = window?.rootViewController {
offerwall.launch(viewController: rootVC)
}
}
}
Step 4: Using the Service in Your Flutter App
// lib/screens/rewards_screen.dart
import 'package:flutter/material.dart';
import '../services/perkox_service.dart';
class RewardsScreen extends StatefulWidget {
final String playerId;
const RewardsScreen({super.key, required this.playerId});
@override
State<RewardsScreen> createState() => _RewardsScreenState();
}
class _RewardsScreenState extends State<RewardsScreen> {
late PerkoxService _perkox;
String _rewardMessage = '';
bool _isLoading = false;
static const String _appId = 'YOUR_APP_ID';
static const String _sdkKey = 'YOUR_SDK_KEY';
@override
void initState() {
super.initState();
_perkox = PerkoxService(
onReward: (reward) {
setState(() {
_rewardMessage = 'Reward: ${reward['amount']} (${reward['status']})';
});
},
onClose: () => setState(() => _isLoading = false),
);
_perkox.init();
}
@override
void dispose() {
_perkox.dispose();
super.dispose();
}
void _showOfferwall() {
setState(() => _isLoading = true);
_perkox.showOfferwall(appId: _appId, sdkKey: _sdkKey, playerId: widget.playerId);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Earn Rewards')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Complete offers to earn rewards'),
const SizedBox(height: 24),
FilledButton(
onPressed: _isLoading ? null : _showOfferwall,
child: const Text('View Offers'),
),
if (_rewardMessage.isNotEmpty) ...[
const SizedBox(height: 16),
Text(_rewardMessage, style: const TextStyle(color: Colors.green)),
],
],
),
),
);
}
}
Step 5: Configure Server-Side Postback
SDK callbacks are for UI feedback. The actual reward credit must happen on your server.
Postback Flow
User completes offer
→ Perkox validates conversion
→ Perkox sends postback to your server
→ Your server verifies {player_id}, {status}, {click_id}
→ Your server credits the user
Configure the Postback URL
In the Perkox Publisher Dashboard, navigate to Apps → Postback URL Configuration and set your endpoint:
https://yourdomain.com/perkox/postback?user_id={player_id}&click_id={click_id}&offer_id={offer_id}&reward={reward_amount}&payout={payout}&status={status}
Supported Postback Placeholders
| Placeholder | Description | Example |
|---|---|---|
{player_id} |
Player ID passed to the SDK | player123 |
{click_id} |
Unique click identifier | d4246e2ca2894efc79df7b4b4 |
{offer_id} |
Completed offer ID | 10101 |
{reward_amount} |
Reward amount to credit | 50.00 |
{payout} |
Publisher payout amount | 10.00 |
{status} |
Conversion status | approved |
Handle the Postback on Your Server (Node.js Example)
app.get('/perkox/postback', async (req, res) => {
const { user_id, click_id, offer_id, reward, payout, status } = req.query;
if (!user_id || !click_id || !status) {
return res.status(400).send('Missing required fields');
}
if (status !== 'approved') {
return res.status(200).send('OK');
}
// Prevent duplicate credits
const existing = await db.query(
'SELECT * FROM rewards WHERE click_id = $1', [click_id]
);
if (existing.rows.length > 0) {
return res.status(200).send('OK');
}
// Credit the user
await db.query(
'INSERT INTO rewards (user_id, click_id, offer_id, amount, payout, status) VALUES ($1, $2, $3, $4, $5, $6)',
[user_id, click_id, offer_id, parseFloat(reward), parseFloat(payout), status]
);
await db.query(
'UPDATE users SET balance = balance + $1 WHERE id = $2',
[parseFloat(reward), user_id]
);
res.status(200).send('OK');
});
Reward Statuses
| Status | Meaning | Action |
|---|---|---|
pending |
Reward under review | Do not credit yet |
approved |
Reward validated | Credit the user |
rejected |
Reward not approved | Do not credit |
reversed |
Previously approved reward reversed | Remove or adjust credit |
Common Pitfalls
1. Relying on SDK callbacks for reward credits. The onReward callback only fires while the offerwall is open. Always use server-side postbacks for the actual reward credit.
2. Using a random Player ID. The playerId must be stable across sessions. If it changes, your backend can’t identify which user to credit.
3. Not preventing duplicate credits. Postbacks can be sent more than once. Always check for duplicate click_id values.
4. Hardcoding API Key in the app. Your appId and sdkKey are safe in the app. Your apiKey is not — use it server-side only.
5. Not handling the ‘reversed’ status. If a reward is reversed, you need to remove or adjust the credit.
Testing Checklist
- SDK initializes without errors on Android and iOS
- Offerwall launches and displays offers
onRewardcallback fires in DartonClosecallback fires when offerwall is dismissed- Postback URL is configured in the Perkox dashboard
- Postback endpoint returns HTTP 200
- Postback handler validates
status === 'approved'before crediting - Duplicate
click_idis blocked playerIdmatches between SDK and postback
Frequently Asked Questions
Do I need to write separate code for Android and iOS?
The Dart service layer is shared. The native platform channel handlers (Kotlin for Android, Swift for iOS) are different but follow the same pattern. You need both.
Can I use this approach without a native SDK?
Yes — you could use a WebView. However, the user experience is worse. Platform channels give you native performance and reliability.
What if the Perkox SDK doesn’t have a Flutter package on pub.dev?
The MethodChannel pattern shown here works with any native SDK. This is the standard Flutter approach for platform-specific libraries.
How do I test the postback endpoint?
Configure your postback URL in the Perkox dashboard, complete a test offer, and check your server logs for the incoming GET request.
Further Reading
- What is Rewarded Monetization? The Definitive Guide →
- How Reward Validation Works: Server-Side Architecture →
- Offerwall SDK Integration: React Native Tutorial →
- Tapjoy Alternatives: 5 Platforms Compared →
- Perkox Android SDK Documentation →
- Postback URL Configuration →
Perkox provides rewarded monetization infrastructure — SDKs, tracking, analytics, and reward validation — for mobile apps and games across Android, iOS, Unity, Flutter, and React Native. Get started →
