Offerwall SDK Integration: Flutter Complete Tutorial

·

Offerwall Placement Strategy

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:

  1. A Perkox publisher account. Sign up at pub.perkox.com.
  2. Your App ID and SDK Key. Available in the Perkox Publisher Dashboard.
  3. Flutter 3.10+ with Dart 3.0+
  4. Android setup: minSdk 21+, Kotlin 1.9+, Java 17
  5. 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 MethodChannel name com.perkox.offerwall must 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 playerId is 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
  • onReward callback fires in Dart
  • onClose callback 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_id is blocked
  • playerId matches 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


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 →

Start monetizing your app with Perkox.

One SDK. Android, iOS, React Native, Flutter, Unity. A premium reward layer for your non-paying users — live in about 10 minutes.

Related articles