Skip to main content

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.

important

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.

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

ParameterTypeDescription
modifierModifierApplied to the root container.
onScanReceiptsuspend () -> UnitCalled 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) -> UnitEmits Browsing and Scanning states for analytics or UI coordination.
When to use

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

ParameterTypeDescription
onScanReceiptsuspend () -> ScanReceiptResult?Must return scan results or null if cancelled.
onContinue() -> UnitCalled when the user taps "Continue" on the receipt summary screen.
onException(String) -> UnitCalled on errors with a user-facing message.
When to use

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 ScanResults to 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"
Runtime safety

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 TypeWhen It FiresDescription
Rewards.ScanFinishedReceipt scan completes successfullyamount contains the base scan reward configured via ScanReward.
Rewards.PromotionReceipt items match a promotional offeramount contains the points earned.
Rewards.BoostUser completes a rewarded adamount contains the points earned.
Session lifecycle

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.

warning

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)

important

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

Coming soon

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.txt file 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

warning

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 (OffersWall or OffersWallFlow)
  • Receipt scan completing and showing reward summary
  • Boost ads displaying (use TestOptions.Ads for test ad units)
  • Webhook endpoint receiving ReceiptProcessed and RewardUpdate events
  • Missed earnings correction flow accessible
  • Debug/test options disabled for production build
  • ActivationClient.close() called in onTerminate()
  • app-ads.txt configured 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.

PitfallWhy It HappensRecommended Approach
Using ReceiptProcessed to calculate rewardsReceiptProcessed contains promotion match data, so it looks like the right sourceUse RewardUpdate webhooks for all backend-calculated rewards. Use the SDK rewards flow only for real-time display.
Assuming one webhook per receiptSeems logical that one scan = one notificationA single receipt can trigger separate webhooks for promo, boost, UGC, and missed earnings. Handle multiple events per blink_receipt_id.
Not designing for idempotencyAssumes each webhook is delivered exactly onceThe 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_idTreating it as a transient valueblink_receipt_id is the primary key that links ReceiptProcessed, RewardUpdate, and on-device scan context. Store it.
Assuming reward finality after scanUser sees rewards on screen, so it feels completeMissed earnings can add rewards hours or days later. Design for open-ended reward accrual per receipt.
Treating the SDK callback as the only reward channelThe callback is immediate and feels authoritativeThe SDK handles real-time display only. Promo, boost, UGC, and missed earnings come exclusively via webhook.
Not handling webhook failuresAssumes delivery is guaranteedWebhooks retry 3 times over ~65 seconds, then go to a dead letter queue. Build monitoring for missing expected webhooks.