Android Activation Integration
SDK Initialization
When the Activation SDK is on the classpath, BlinkReceiptSdk.initialize() automatically detects it and configures the ActivationClient with deviceId, clientUserId, and appBundleId. Set user identity in the onComplete callback.
Kotlin
import com.microblink.BlinkReceiptSdk
import com.actualplatform.activation.ActivationClient
import okio.ByteString.Companion.encodeUtf8
BlinkReceiptSdk.initialize(context, object : InitializeCallback {
override fun onComplete() {
// ActivationClient is now configured with device identity.
// 1. Configure user identification (at least one required)
ActivationClient.instance.apply {
hashedEmail = userEmail.encodeUtf8().sha256().hex()
hashedPhone = userPhone.encodeUtf8().sha256().hex() // optional
}
// 2. Configure in-app currency
ActivationClient.instance.apply {
rewardCurrencyName = "Points"
rewardPayoutPercentage = 100.0
}
// 3. Configure base scan reward
ActivationClient.instance.scanReward = ScanReward(reward = 10f)
}
override fun onException(throwable: Throwable) {
Log.e("Init", "SDK init failed", throwable)
}
})
Java
BlinkReceiptSdk.initialize(context, new InitializeCallback() {
@Override
public void onComplete() {
ActivationClient client = ActivationClient.getInstance();
String hash = ByteString.encodeUtf8(email).sha256().hex();
client.setHashedEmail(hash);
}
@Override
public void onException(@NonNull Throwable throwable) {
Log.e("Init", "SDK init failed", throwable);
}
});
The SDK debounces property changes. Rapid sequential sets (e.g., the receipts SDK setting deviceId followed by the host app setting hashedEmail) coalesce into a single registration call.
At least one of hashedEmail or hashedPhone must be set before displaying the offer wall. The offer wall will not load promotions without a registered user.
Presenting the Offer Wall
The Offer Wall is the primary entry point for user engagement. There are two integration options depending on your app's navigation approach.
Option A: OffersWall (Recommended)
OffersWall renders the promotions offer wall as an embeddable composable that fits within your existing navigation stack. The host app retains full control over the navigation chrome (toolbar, back handling) and the scan experience.
When the user taps "Scan Receipt", the onScanReceipt callback fires and the host app launches its own camera flow. Use .activation(true) on the camera contract to let the SDK handle the post-scan experience (loading screen, receipt summary, rewards) inside the camera activity.
import com.actualplatform.activation.OffersWall
@Composable
fun PromotionsScreen(onBack: () -> Unit) {
OffersWall(
onScanReceipt = {
// Launch your camera or scan flow here
},
onDismiss = onBack,
)
}
OffersWall Parameters
| Parameter | Type | Description |
|---|---|---|
modifier | Modifier | Applied to the root container. |
onScanReceipt | suspend () -> Unit | Called when the user taps "Scan Receipt". Launch your camera flow here. |
onDismiss | (() -> Unit) | Called on back navigation. When null, the SDK header is hidden. |
onNavigationStateChanged | (OffersWallNavigationState) -> Unit | Emits Browsing and Scanning states for analytics or UI coordination. |
You have an existing navigation stack and want to embed the offer wall as one destination, control the toolbar and back button, and use .activation(true) on the camera contract.
Option B: OffersWallFlow
OffersWallFlow is a full-screen, self-contained composable that manages the entire promotions experience: offer wall, ads loading, and receipt summary. The SDK owns all internal navigation between these screens.
The key difference: onScanReceipt is a suspend function that must return a ScanReceiptResult (or null if cancelled). The SDK uses this result to drive the internal loading and receipt summary screens.
import com.actualplatform.activation.OffersWallFlow
import com.actualplatform.activation.ScanReceiptResult
@Composable
fun FullPromotionsFlow(onFinish: () -> Unit) {
OffersWallFlow(
onScanReceipt = {
launchCameraAndAwaitResult()
},
onContinue = onFinish,
onException = { exception: ActivationException -> Log.e("Activations", "${exception.message}") },
onDismiss = onFinish,
)
}
Additional OffersWallFlow Parameters
| Parameter | Type | Description |
|---|---|---|
onScanReceipt | suspend () -> ScanReceiptResult? | Must return scan results or null if cancelled. |
onContinue | () -> Unit | Called when the user taps "Continue" on the receipt summary screen. |
onException | (String) -> Unit | Called on errors with a user-facing message. |
You want a dedicated full-screen promotions experience with no host app navigation chrome. Do not use .activation(true) on the camera contract with this option (OffersWallFlow handles post-scan itself).
Starting a Receipt Scan
When using OffersWall with the BlinkReceipt Camera UI, enable the activation post-scan flow on the camera contract:
val launcher = rememberLauncherForActivityResult(
contract = CameraRecognizerContract(),
) { result ->
when (result) {
is CameraRecognizerResults.Success -> {
scanResults = result.results
}
is CameraRecognizerResults.Exception -> {
Log.e("Scan", "Error: ${result.exception}")
}
CameraRecognizerResults.Cancelled -> {
// User cancelled the scan
}
}
}
// Launch the camera with activation enabled
launcher.launch(
CameraRecognizerOptions.Builder()
.options(scanOptions)
.characteristics(cameraCharacteristics)
.activation(true) // Enables post-scan activation flow
.build()
)
When .activation(true) is set, the camera fragment automatically:
- Scans the receipt
- Maps
ScanResultsto the Activations SDK model - Displays the ads loading screen with promotions
- Shows the receipt summary with matched offers and rewards
- Returns to the host app when the user taps "Continue"
If the Activations SDK is not present at runtime, the .activation(true) flag is silently ignored and the camera returns results normally.
Reward Callback Reference
On-Device Rewards (SDK)
The ActivationClient exposes a rewards SharedFlow that emits reward events as they occur during a scan session:
import com.actualplatform.activation.ActivationClient
import com.actualplatform.activation.Rewards
LaunchedEffect(Unit) {
ActivationClient.instance.rewards.collect { reward ->
when (reward) {
is Rewards.ScanFinished -> {
Log.d("Rewards", "Base scan reward: ${reward.amount}")
}
is Rewards.Promotion -> {
Log.d("Rewards", "Promotion earned: ${reward.amount}")
}
is Rewards.Boost -> {
Log.d("Rewards", "Boost earned: ${reward.amount}")
}
}
totalRewards += reward.amount
}
}
| Reward Type | When It Fires | Description |
|---|---|---|
Rewards.ScanFinished | Receipt scan completes successfully | amount contains the base scan reward configured via ScanReward. |
Rewards.Promotion | Receipt items match a promotional offer | amount contains the points earned. |
Rewards.Boost | User completes a rewarded ad | amount contains the points earned. |
Reward state is scoped to a scan session. When a new scan session starts, the SDK starts a new session internally. Clear your local reward accumulators when starting a new flow.
The Android SDK supports base scan rewards via ActivationClient.instance.scanReward = ScanReward(reward = 10f), configured during initialization. The Rewards.ScanFinished event emits through the rewards SharedFlow when a receipt scan completes, returning the configured base reward amount. This is equivalent to the iOS "ScanFinished" callback context.
Server-Side Rewards (Webhooks)
The on-device SDK rewards flow is not the only reward channel. Promotional rewards, boost rewards, UGC rewards, and missed earnings rewards are calculated by Actual's backend and delivered to your server via webhooks. Both channels are required:
- SDK rewards flow — for real-time in-app reward display
- Webhooks — for reliable server-side tracking, auditability, and crediting users
See the Webhooks section below for full details.
Receipt Validation
The SDK validates receipts automatically before processing rewards. Validation checks include:
- Duplicate detection — Receipts already submitted are rejected (cross-client, 14-day window).
- Fraud detection — Suspicious receipts are flagged.
- Receipt age — Receipts older than 14 days are rejected.
- Receipt date — Receipts without a parseable date are rejected.
All validation is handled internally by the SDK. No configuration is required.
Missed Earnings
If a receipt scan does not initially match promotions (e.g., due to OCR quality or missing items), the user may be presented with a missed earnings correction flow. This allows users to manually verify or correct scanned items to recover eligible rewards.
Missed earnings rewards are delivered via RewardUpdate webhooks and may arrive hours or days after the original scan.
Ad Monetization Setup (Google Ad Manager)
Actual Activation uses Google Ad Manager (GAM) to serve ads within the SDK experience. This includes ads shown during receipt processing and rewarded ads that trigger boost rewards. See the GAM MCM Setup guide for account configuration.
What GAM Is Used For
- Serve ads during the loading screen (while receipts are processing)
- Serve rewarded ads that users can watch to earn additional rewards (boosts)
- Enable monetization through advertiser demand integrated into the SDK
Ad rendering and placement are handled by the SDK. No UI implementation is required on your side.
What You Need to Provide
- An active Google Ad Manager account
- Complete MCM (Multiple Customer Management) onboarding if applicable
- Configure your
app-ads.txtfile with the required publisher ID - Provide required account details to the Actual team during onboarding
Your team may need to coordinate with internal stakeholders (e.g. finance or IT) to access or verify your existing GAM account.
What Actual Manages
- Ad demand sourcing and optimization
- Ad placement within the SDK experience
- Reward logic tied to ad completion (e.g. boost rewards)
- Integration between ad events and reward generation
No direct integration with GAM APIs is required from your app.
Debug Mode
These options are intended for development and testing only. Do not enable them in production builds.
Test Options
import com.actualplatform.activation.networking.TestOptions
ActivationClient.instance.testOptions = setOf(
TestOptions.Ads, // Use Google Ad Manager test ad units
TestOptions.Test, // Enable test mode (returns test promotions)
)
Debug Placements
Force specific offer wall layouts to test different UI configurations:
import com.actualplatform.activation.models.PlacementLayout
ActivationClient.instance.placements = setOf(
PlacementLayout.OFFER_WALL_CAROUSEL,
PlacementLayout.OFFER_WALL_LIST,
PlacementLayout.OFFER_WALL_GRID,
)
Logging
Enable SDK debug logging by adding the following to your project's local.properties:
logcat.state=enabled
This activates verbose logging across the BlinkReceipt and Activations SDKs, including network requests, scan session lifecycle, reward events, and receipt validation results.
Cleanup
Close the ActivationClient when your application terminates:
Kotlin
override fun onTerminate() {
BlinkReceiptSdk.terminate()
ActivationClient.instance.close()
super.onTerminate()
}
Java
@Override
public void onTerminate() {
BlinkReceiptSdk.terminate();
ActivationClient.getInstance().close();
super.onTerminate();
}
View System Integration
If your app uses traditional XML layouts rather than Compose, embed the offer wall via ComposeView:
findViewById<ComposeView>(R.id.compose_offers_wall).apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MaterialTheme {
OffersWall(
onScanReceipt = { /* launch camera */ },
onDismiss = { /* handle back */ },
)
}
}
}
Add the ComposeView to your XML layout:
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_offers_wall"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Testing Checklist
- BlinkReceipt SDK initialized and Activations SDK detected automatically
- At least one user identifier set (hashed email or hashed phone)
- Currency name and payout percentage configured
- Reward flow collecting events via
ActivationClient.instance.rewards - Offer Wall presenting correctly (
OffersWallorOffersWallFlow) - Receipt scan completing and showing reward summary
- Boost ads displaying (use
TestOptions.Adsfor test ad units) - Webhook endpoint receiving
ReceiptProcessedandRewardUpdateevents - Missed earnings correction flow accessible
- Debug/test options disabled for production build
ActivationClient.close()called inonTerminate()app-ads.txtconfigured and publicly accessible (see GAM MCM Setup)
Integration Pitfalls to Avoid
These are the most frequent issues encountered during integration. Reviewing this list before launch can save significant debugging time.
| Pitfall | Why It Happens | Recommended Approach |
|---|---|---|
Using ReceiptProcessed to calculate rewards | ReceiptProcessed contains promotion match data, so it looks like the right source | Use RewardUpdate webhooks for all backend-calculated rewards. Use the SDK rewards flow only for real-time display. |
| Assuming one webhook per receipt | Seems logical that one scan = one notification | A single receipt can trigger separate webhooks for promo, boost, UGC, and missed earnings. Handle multiple events per blink_receipt_id. |
| Not designing for idempotency | Assumes each webhook is delivered exactly once | The same webhook can be delivered more than once during retries. Use reward_id or blink_receipt_id + reward_type to deduplicate. |
Not storing blink_receipt_id | Treating it as a transient value | blink_receipt_id is the primary key that links ReceiptProcessed, RewardUpdate, and on-device scan context. Store it. |
| Assuming reward finality after scan | User sees rewards on screen, so it feels complete | Missed earnings can add rewards hours or days later. Design for open-ended reward accrual per receipt. |
| Treating the SDK callback as the only reward channel | The callback is immediate and feels authoritative | The SDK handles real-time display only. Promo, boost, UGC, and missed earnings come exclusively via webhook. |
| Not handling webhook failures | Assumes delivery is guaranteed | Webhooks retry 3 times over ~65 seconds, then go to a dead letter queue. Build monitoring for missing expected webhooks. |