Actual Activation Overview
What is BlinkEngage?
BlinkEngage is the SDK for Actual's Activation Platform. This SDK sits on top of the BlinkReceipt SDK (Paper Receipt) and adds rewards, offers, and ad monetization to your receipt scanning flow. BlinkEngage takes over after the scan — loading screen, receipt summary, boost ads, the works.
What It Does
Out-of-the-box User Activation Journeys
The SDK supports five distinct user journeys, each representing a different way users can engage and earn rewards:
| Journey | Description |
|---|---|
| Offer Discovery & Clipping | Browse and clip available offers |
| Receipt Scanning | Scan receipts to earn on qualifying purchases |
| Receipt Boosters (Watch & Earn) | Watch ads for bonus rewards |
| Product Capture UGC Tasks (Snap & Earn) | Photograph products for extra rewards |
| Missed Earnings | Correct receipt data for potential credits |
Offer Wall
A pre-built screen that shows coupons and deals to your users. Supports grid, list, and carousel layouts and controlled server side by Actual with dynamic capabilities. Users can filter by store, clip offers to save them, and kick off a receipt scan from a floating action button.
The offer wall manages requirements for offer details and catalogue management on behalf of the publisher app. It should be presented in a regularly accessible location within the host app. Ideally dedication navbar location. To surface offers elsewhere in your app, such as a carousel on your home screen, use the Client-Side Offer Feed.
Two modes:
.all— Every available offer.clipped— Only what the user saved
Client-Side Offer Feed
The Client-Side Offer Feed lets you show the SDK's offers in your own UI, such as a carousel on your home screen or a section in your rewards tab. Your app asks the SDK for the feed and gets back the same curated offers the offer wall shows, as plain data: groups, each with its offers, ranked by Actual and with reward values already in your currency. When the shopper taps one of your cards, your app hands the offer or group back to the SDK, which opens its own offer details or the group's full list.
Your app owns the layout. The SDK still owns the data, the offer details, clipping, and scanning.
| Platform | Available from | Guide |
|---|---|---|
| iOS | 1.14.0 | iOS Client-Side Offer Feed |
| Android | Coming in a future release | — |
See Starting from your app's own UI for how your app and the SDK hand the shopper back and forth.
Receipt Scanning
Engage your users and collect valuable purchase data. Actual fully-manages the user experience and data extraction of scanning a paper receipt from any retailer and turning it into structured data.
Receipt Boosters and Product Capture
Once a user successfully scans a receipt, we'll keep them engaged with more ways to earn (and drive revenue for you) such as watching targeted ads (Watch & Earn) or snapping product labels and barcodes (Snap & Earn).
Missed Earnings
For users who have challenges with receipt submission or reward redemption, we provide an out-of-the-box flow for users to re-submit their receipt to claim any missed earnings.
Rewards
You configure three properties and the SDK handles the rest:
currencyName— What to call your currency ("points", "coins", etc.). Default:"points"currencyPerDollar— Conversion rate. Default:100.0($1 = 100 points)userPayoutPercentage— Share of boost/UGC rewards the user gets (40%–100%). Default:0.6. In current versions, this is a shared property. Best practice guidance is to use the default 60%.
On iOS (1.12.0+), the base scan reward is set by your app unless Actual has configured an amount for the receipt's store. See Store-specific base scan reward.
Theming
Implement the Theme protocol to swap out colors, fonts, and icons across every screen. Return nil for any key to keep the SDK default. See the iOS Activation Customization guide for the full reference of color, font, and icon keys.
How the Scan Flow Works
1. User Scans a Receipt
You start the BlinkReceipt camera as usual. When enableBlinkEngage = true, the SDK takes it from there — no need to handle scan results yourself.
2. Loading Screen
While the backend processes the receipt, users see a loading drawer with a progress bar and an ad slot (native or banner). The progress runs in two stages: 0%→80% while waiting for data, then 80%→100% when results come back.
Status messages rotate through: "Scanning receipt", "Qualifying purchases", "Loading offers".
3. Receipt Summary Screen
Shows what the user earned:
| Reward Type | What It Is |
|---|---|
| Base Scan Reward | Points your app assigns for completing a scan (via rewardCallback), unless Actual has configured an amount for the receipt's store (details)Webhook: base_scan |
| PVP Offer Rewards | Points earned (with statuses streamed back via webhook) for promotions related to purchased items on the receipt Webhook: promo |
| Boost Rewards | Bonus points from watching a rewarded video ad Webhook: boost |
| UGC Task Rewards | Points paid by Actual for scanning a product barcode and taking a photo Webhook: barcode_collection |
The reward counter animates from 0 to the total, followed by a confetti effect. Qualified products get highlighted in the list.
4. Boost Opportunities
After the summary loads, users can earn more by:
- Watching a short video ad
- Completing a CPA offer (survey, app install, etc.)
- Scanning a product barcode for a UGC task
5. Missed Earnings
If user feels they were not granted a reward for something they expected, they can submit a claim to remedy it:
- Tap the receipt icon on the summary header
- Edit merchant name (with auto-suggest), date (inline picker), or product details
- Add missing products by scanning their barcode
- Submit — corrected items turn green and show "Pending Review"
Reward Callback
Your app needs to implement one closure on BlinkEngageSDK.shared.rewardCallback. While your primary accounting should be based on webhook, these callbacks are provided for the sake of UI continuity immediately after the scan and we advise you reconcile it with the webhook when the webhook is received.
It gets called in four situations:
| Context | Who's Talking | What To Do |
|---|---|---|
"ScanFinished" | SDK asks your app | Return an NSNumber with the base scan reward. A store-specific amount configured by Actual replaces it. |
"Promo" | SDK tells your app | Credit rewardAmount to the user |
"Boost" | SDK tells your app | Credit rewardAmount to the user |
"BarcodeCollection" | SDK tells your app (iOS only for now; Android will emit it once User Generated Content is supported there) | Credit rewardAmount to the user |
If you don't set this callback, base scan reward defaults to 0. The SDK still calculates and displays promo/boost/UGC rewards on screen, but your app won't know to credit them.
BlinkEngageSDK.shared.rewardCurrencyName = "points"
BlinkEngageSDK.shared.rewardCurrencyPerDollar = 100.0
BlinkEngageSDK.shared.userPayoutPercentage = 0.5
Reward Examples
Base Scan Reward
- BlinkEngage SDK invokes the
rewardCallbackto ask host app how much to reward for the current scan - Host app responds with a number specifying the base scan reward (if any) in in-app currency
- This amount is used in 2 ways:
- It is displayed (along with any other rewards) in the Receipt Summary Screen (RSS) to indicate to the user how many points they received for the current scan
- It is converted to a dollar amount by dividing by your
currencyPerDollarsetting and then this dollar amount is sent to your webhook
- The
userPayoutPercentageproperty has no impact on this reward type - Actual can configure an amount for a store that supersedes the host app's amount (see Store-specific base scan reward below)
- Example:
- User finishes scan
- BlinkEngage SDK calls
rewardCallbackwithcontext=ScanFinished - Host app returns
1500 - BlinkEngage displays 1500 Points at the top of the RSS
- Webhook sent with
rewardValue: 15.00
Store-specific base scan reward
Actual can configure a base scan reward for a specific store, or a default for all of your stores. A configured amount supersedes the amount your app supplies for receipts from that store, and it can be lower than yours, including zero.
How it works:
- When a receipt starts processing, the SDK reports your amount to Actual along with the store it identified on the receipt.
- Actual answers with the amount to apply: its configured amount for that store, or yours when nothing is configured.
- The Receipt Summary shows the applied amount, and the
base_scanRewardUpdatewebhook carries it. - If Actual's answer doesn't reach the device within a few seconds, the Receipt Summary shows your amount instead. The webhook still carries the amount Actual applied, so credit the shopper from the webhook, not from the screen.
| Platform | Available from | How your app learns the applied amount |
|---|---|---|
| iOS | 1.12.0 | From the base_scan webhook. rewardCallback is asked for your amount before Actual's answer arrives, so it never sees the replacement. |
| Android | Coming in a future release | — |
- Example:
- Your app returns
1500for every scan (currencyPerDollaris100) - Actual has configured $0.50 for Store X
- User scans a Store X receipt
- BlinkEngage displays 50 Points at the top of the RSS
- Webhook sent with
rewardValue: 0.50 - A receipt from any other store still earns 1500 Points (
rewardValue: 15.00), as long as no default is configured for all stores
- Your app returns
Offer Validations
- All SKU-based offers have a fixed dollar amount associated with them, which must be paid out entirely to the user
- Thus, when a user opens the offer wall in your app, BlinkEngage takes the dollar amount of each offer, multiplies by your
currencyPerDollarand displays the resulting points value in the card for each offer - When a user scans a receipt with qualifying products for a given offer, the user sees the corresponding points value on the RSS in-line with the qualifying product
- The webhook sent to your backend contains the dollar amount for that offer
- The
userPayoutPercentagehas no impact on this reward type - Example:
- An offer for Colgate Toothpaste has a dollar value of $2
- The user sees this offer on the offer wall as 200 points
- If the offer scans a qualifying receipt, on the RSS they will see 200 points next to that line item
- BlinkEngage SDK calls
rewardCallbackwithcontext=PromoandrewardAmount=200 - Webhook sent with
rewardValue: 2.00for this offer
Boosts (Watch & Earn / Snap & Earn)
- On the RSS we will occasionally present the user with opportunities to “boost” their rewards for that receipt by either watching a rewarded ad (Watch & Earn), or performing a product collection task (Snap & Earn - i.e. snap a picture of the product and scan its barcode)
- We set the dollar reward amount for these boosts, and we multiply them by both
currencyPerDollarANDuserPayoutPercentage(if any) to determine the number of points to show alongside the boost CTA - The webhook sent to your backend contains the dollar amount for the boost
- Example:
- A Watch & Earn boost has a reward value of $1
- User sees this CTA on the Receipt Summary Screen as 50 points ($1 x 100 x 0.5)
- The user successfully watches the ad
- User sees 50 points added to their tally on the Receipt Summary Screen
- BlinkEngage calls
rewardCallbackwithcontext=BoostandrewardAmount=50 - Webhook is sent with
rewardValue: 1.00
Error Flows
A receipt that can't be processed is not a dead end. Instead of ending the scan with an error, the SDK raises a screen that explains what happened and — in every case — offers to send the receipt to a human reviewer. Dismissing that screen returns the shopper to the offer wall with the flow intact, so they can scan again right away.
Only genuine failures still surface as errors: an API timeout or a dropped connection shows an error dialog ("Oops!") and is retried, not reviewed.
Where each check runs
Receipt checks run in two tiers, and the tier decides whether the receipt is ever registered with our backend at all.
| Tier | When it runs | What it checks | Registered server-side? |
|---|---|---|---|
| On device | Before the scan is submitted — no network | Unreadable photo, missing required field, purchase date outside the rewards window | No |
| Server | After the scan is submitted for processing | Submission limits, receipt age, fraud flags, duplicates | Yes |
Because the device-tier checks run before submission, a receipt one of them stops is never registered — so it can never come back as a duplicate or pick up a fraud flag either. Receipt age is the one rule checked in both tiers: on device from the date read off the photo, and again by the backend from its own records. Both land on the same screen.
The flows
| What happened | Detected | What the shopper sees | Retake offered? | Where it ends |
|---|---|---|---|---|
| Photo can't be read | On device | Invalid-receipt screen, with a checklist of what to check | Yes | Offer wall |
| Store, date, or total missing | On device | A short form asking for what's missing | Yes | Continues to processing → Receipt Summary |
| Purchase date outside the rewards window | On device or server | Receipt-too-old screen with a date-correction form | No | Offer wall |
| Receipt matches an earlier submission | Server | Receipt Summary with a duplicate banner; rewards suppressed | — | Receipt Summary |
| Receipt carries an actionable fraud flag | Server | Receipt Summary with an invalid banner; rewards suppressed | — | Receipt Summary |
| A submission limit was hit | Server | Frequency-cap screen naming each limit and its value | — | Offer wall |
Two details worth knowing:
- A missing store name alone never blocks a scan. The store is readable from the photo during review, so asking for it up front would stop the shopper for no benefit. A receipt missing only the store goes straight to processing with the store left blank. A missing date or total — alone, or alongside the store — does show the form.
- The receipt-too-old screen is the only one with no retake. A newer photo can't change a purchase date that has already been read, so a review request is the only way forward.
Precedence: which screen wins
More than one thing can be wrong at once. The device checks run in a fixed order, and the first one to fail decides the screen:
- Is the photo readable? An unreadable photo says nothing reliable about which fields are present, so this outranks everything else.
- Are the required fields there? An absent or unparseable date can't be age-checked, so this comes before the window check.
- Is the purchase date inside the rewards window? 14 days by default.
- Otherwise the scan is accepted and continues to processing.
Server verdicts have their own fixed order — submission limit, then age, then fraud, then duplicate — so a receipt that is both a duplicate and outside the window reads as outside the window, whichever tier caught it.
What a review request carries
When a shopper fills in a missing field or disputes a verdict, that value is evidence for a reviewer — it is never merged into the scan. The scan data submitted for processing is always exactly what the scanner read.
The practical consequence: a field that was missing when the receipt was scanned is still missing on the Receipt Summary, even though the shopper just typed it in. Only a reviewer, acting on the review request, can change the value the receipt is ultimately paid out against. Design your own screens the same way — treat a correction as a pending claim, not as corrected data.
One value is validated before it can be submitted at all: a purchase date typed into the form is age-checked as it is entered. A date still outside the rewards window keeps the submit button disabled with an inline message, because no reviewer could make that receipt eligible.
When the scan reward lands
A scan that ends at a review request earns nothing at scan time. The backend holds the reward and grants it only if a reviewer accepts the receipt, which can be hours or days later.
| Flow | Base scan reward |
|---|---|
| Scan accepted normally | Paid immediately |
| Missing fields, review submitted | Paid immediately — this flow continues on to processing |
| Invalid receipt, receipt too old, submission limit, disputed duplicate | Deferred — paid only if a reviewer accepts |
Deferred rewards arrive on your server as RewardUpdate webhooks, the same channel as missed earnings. Build for open-ended reward accrual per receipt rather than assuming rewards are final when the shopper leaves the summary.
Retakes keep the same receipt
Every screen that offers a retake reopens the camera on the same receipt, not a new one. The extra photo is another angle on one purchase, so it keeps the same receipt identifier and the accumulated frames travel with it; the updated scan then re-enters the checks above from the top. A shopper can retake, get a different screen, retake again, and it is all still one receipt as far as rewards, reviews, and webhooks are concerned.
Fraud flags are not automatic rejection
A fraud flag on its own is not actionable. The backend may flag a receipt for soft or informational reasons — scan velocity, for example — and those receipts process and pay out normally.
A receipt is only treated as invalid when it is flagged and carries one of these five reasons: receipt_counterfeit_text, receipt_merchant_mismatch, user_geo_anomaly, user_identity_anomaly, ip_anomaly. Even then it is not turned away: the Receipt Summary renders with rewards suppressed and the dispute stays open.
Debug Mode
For development, turn on test ads so you don't serve real ones:
BlinkEngageSDK.shared.debugModeEnabled = true
Turn this off before shipping.
Architecture
The SDK is organized into self-contained modules:
| Module | What It Does |
|---|---|
OfferWallScreen | Offer display, store filtering, clipping |
AdLoadingScreen | Loading drawer with progress bar and ad slot |
ReceiptSummaryScreen | Scan results, rewards, boost offers |
MissedEarningsScreen | Receipt correction UI |
StoresScreen | Store list and filtering |
ProductCaptureScreen | Barcode scanning and photo capture for UGC |
Patterns used: MVVM for each module, a Coordinator layer (Combine-based) for presentation flow, and a shared service layer for networking, image caching, logging, and Google Ads.
Network handling: built-in NWPathMonitor for connectivity checks, and URLSession configured with waitsForConnectivity = true.