Skip to main content

Android Email Linking

The BlinkReceipt Digital SDK parses e-receipts from a growing list of retailers and four mail providers: Gmail, Outlook, Yahoo, and AOL. The procedure for integrating each provider is slightly different. This guide outlines the steps to integrate and authenticate a user account for each mail provider, then the common methods used to invoke e-receipt parsing.

Requirements

  • AndroidX
  • Min SDK 23+
  • Compile SDK 36+
  • Java 17+

Installation

1. Add the Maven Repository

Ensure mavenCentral() is included in your repositories block:

repositories {
// ...
mavenCentral()
}

2. Add Dependencies

dependencies {
implementation(platform("com.microblink.blinkreceipt:blinkreceipt-bom:1.8.3"))

implementation("com.microblink.blinkreceipt:blinkreceipt-digital")
implementation("com.microblink.blinkreceipt:blinkreceipt-recognizer")
}

3. Initialize the SDK

Initialize BlinkReceiptDigitalSdk in your Application class:

class BlinkApplication : Application() {

override fun onCreate() {
super.onCreate()

BlinkReceiptDigitalSdk.initialize(this, object : InitializeCallback {
override fun onComplete() { }

override fun onException(e: Throwable) { }
})
}
}

4. Product Intelligence Key

Add to AndroidManifest.xml to enrich parsed receipt data with full product names, brands, and categories:

<meta-data
android:name="com.microblink.ProductIntelligence"
android:value="YOUR_PRODUCT_INTELLIGENCE_KEY" />

Task Framework

The SDK heavily leverages Google's Task for result and exception handling. Most SDK functions return a Task<T> — a reference to a job running on the main or a background thread. T is the type of result the task returns when done.

Add an OnSuccessListener for successful results and an OnFailureListener for exceptions. Listeners can be chained.

val exampleTask: Task<Foo> = repository.fetchFoo()

exampleTask
.addOnSuccessListener { foo ->
// Do something with the result
}
.addOnFailureListener { exception ->
// Handle the error
}

OnCompleteListener<TResult> combines both outcomes into a single callback. Its onComplete(Task<TResult> task) method fires for both success and failure — you must check the task state manually.

warning

Calling task.getResult() while the task is still executing or has already failed will throw IllegalStateException and RuntimeException respectively.

exampleTask.addOnCompleteListener(new OnCompleteListener<Foo>() {
@Override
public void onComplete(Task<Foo> task) {
if (task.isSuccessful()) {
Foo result = task.getResult();
} else {
Exception e = task.getException();
}
}
});

Available status checks: task.isSuccessful(), task.isCompleted(), task.isCanceled().


IMAP (Yahoo and AOL)

Use ImapClient to connect to Yahoo or AOL accounts via IMAP. Gmail has its own dedicated client — see Gmail below.

Android Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".BlinkApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<meta-data
android:name="com.microblink.ProductIntelligence"
android:value="KEY" />

</application>
</manifest>

Theme Requirement

The IMAP authorization UI uses a Material bottom sheet. Your app theme must extend Theme.MaterialComponents.*:

<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

Initialize the Client

ImapClient initialization is asynchronous. Do not access IMAP messages or account information until onComplete() fires.

caution

If you use lazy initialization, it will throw exceptions until the client has been fully initialized. This applies on a per-instance basis.

ImapClient(applicationContext, object : InitializeCallback {
override fun onComplete() { }

override fun onException(throwable: Throwable) { }
})

Provider Setup

After collecting user credentials, start the provider setup workflow, which walks the user through linking their account to the provider:

ProviderSetupDialogFragment.newInstance(
ProviderSetupOptions.newBuilder(
PasswordCredentials.newBuilder(
Provider.GMAIL,
"email@example.com",
"account password"
).build()
).build()
).callback { }.show(supportFragmentManager, TAG)
note

ProviderSetupDialogFragment only supports Yahoo, AOL, and Gmail. Passing an unsupported IMAP provider throws IllegalStateException.

Verify / Login

verify() checks whether the SDK has any cached credentials that can be used without explicit sign-in. Call it without parameters to automatically use cached credentials, or pass PasswordCredentials to verify specific credentials.

The result is a Task<Boolean>. A true result means the credentials grant access to a valid account. An exception means the credentials are invalid.

client.verify(
PasswordCredentials.newBuilder(
Provider.GMAIL,
"test@gmail.com",
"app password"
).build()
).addOnSuccessListener { isVerified ->
// isVerified == true: credentials are valid
}.addOnFailureListener {
// credentials are invalid — prompt re-login
}

Fetch Cached Accounts

accounts() retrieves cached PasswordCredentials from the SDK's encrypted cache. Call this after a successful verify() to get the account details without re-prompting the user. This does not verify the credentials — it only returns what is cached.

client.accounts().addOnSuccessListener { accounts ->
// accounts: List<PasswordCredentials>
}.addOnFailureListener { }

Configure the Client

Set these properties on the client before calling messages():

PropertyTypeDefaultClient FunctionDescription
dayCutoffInt14dayCutoff(int days)Maximum number of days to look back in the inbox for receipts
filterSensitiveBooleanfalsefilterSensitive(boolean)When true, excludes sensitive/adult products from results
subProductsBooleanfalsesubProducts(boolean)Returns sub-products nested under parent items (e.g. "Guacamole" under "Burrito")
countryCodeString"US"countryCode(String)Used for product intelligence classification
sendersToSearchMap<String, Merchant>nullsendersToSearch(Map)Map custom sender addresses to known merchants

The sendersToSearch property is useful when a known retailer sends receipts from a non-obvious email address. For example, Target might send from receipts@uniquetarget.com:

client.sendersToSearch(
mapOf(
"receipts@uniquetarget.com" to Merchant("Target.com", "receipts@uniquetarget.com")
)
)

Read Messages

Call messages(@NonNull MessagesCallback callback) to begin fetching and parsing emails. The SDK runs a series of internal tasks before returning results.

onComplete fires once per linked account — if the user has multiple accounts, it fires multiple times. Each emission includes the PasswordCredentials of the account the results came from. It is entirely possible to receive both onException and onComplete in a single messages() call — a failure on one account does not prevent results from other accounts.

Each item in the returned List<ScanResults> represents one successfully parsed receipt.

fun messages() {
client.messages(object : MessagesCallback {
override fun onComplete(credential: PasswordCredentials, result: List<ScanResults>) {
// result: parsed receipts for this account
// credential: the account these results came from
}

override fun onException(throwable: Throwable) {
// one account failed — others may still return results
}
})
}

Remote Messages (Server-Side Parsing)

remoteMessages(@NonNull JobResultsCallback callback) is similar to messages(), but parses emails on the server rather than on-device. The callback provides the PasswordCredentials of the account and a JobResults object containing the server job ID and success status.

client.remoteMessages(object : JobResultsCallback {
override fun onComplete(credential: PasswordCredentials, result: JobResults) {
// result.jobId, result.isSuccessful
}
})

Logout

Sign a user out and clear their cached credentials. Pass clearCache = true to also clear stored cookies.

client.logout(
PasswordCredentials.newBuilder(
Provider.GMAIL,
"test@gmail.com",
"app password"
).build()
).addOnSuccessListener {
// signed out and credentials cleared
}.addOnFailureListener { }

Clear Last Checked Time

The SDK caches the last search date to avoid re-fetching already-parsed emails. Call clearLastCheckedTime() to reset this cache and force a full re-scan up to dayCutoff days. Returns Task<Boolean>.

client.clearLastCheckedTime().addOnSuccessListener { cleared ->
// cleared == true: cache was reset
}

Destroy the Client

Always call close() in onDestroy() to clean up pending calls and allocated resources:

override fun onDestroy() {
super.onDestroy()
client.close()
}

Exception Reference

FunctionException MessageExplanation
messages()"unable to find provider accounts"No cached accounts found — user must re-link
logout()"unable to store this provider " + providerAccount not found in the cache
verify()"unable to connect to imap service!"Server or credential error — retry or prompt the user to re-enter credentials
remoteMessages()"Unable to encrypt credentials " + usernameCredential encryption failed

Outlook

Register your app in the Microsoft Application Registration Portal, add a Native Application platform, and obtain an Application ID.

Android Manifest

<activity android:name="com.microsoft.identity.client.BrowserTabActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="com.blinkreceipt.development"
android:path="/[Signature Hash]"
android:scheme="msauth" />
</intent-filter>
</activity>

Authentication Configuration

The Outlook client references an auth config file in res/raw/. Create auth_config_single_account.json under res/raw/:

{
"client_id": "[CLIENT_ID]",
"authorization_user_agent": "DEFAULT",
"redirect_uri": "[REDIRECT_URI]",
"account_mode": "SINGLE",
"broker_redirect_uri_registered": true,
"authorities": [
{
"type": "AAD",
"authority_url": "https://login.microsoftonline.com/common"
}
]
}

Initialize the Client

Initialization is asynchronous. Do not access messages or account info until onComplete() fires.

caution

Using lazy initialization will throw exceptions until the client is fully initialized. This is on a per-instance basis.

OutlookClient(
applicationContext,
R.raw.auth_config_single_account,
object : InitializeCallback {
override fun onComplete() { }
override fun onException(throwable: Throwable) { }
}
)

Login

client.login(this).addOnSuccessListener {
// signed in
}.addOnFailureListener { }

Messages

Returns a Task<List<ScanResults>> with parsed receipts from the Outlook mailbox:

client.messages().addOnSuccessListener { results ->
// results: List<ScanResults>
}.addOnFailureListener { }

Logout

client.logout().addOnSuccessListener {
// signed out
}.addOnFailureListener { }

Destroy the Client

override fun onDestroy() {
super.onDestroy()
client.close()
}

Gmail

Initialize the Client

Provide the constructor with a Context and a thread count. When called from a Fragment, pass requireActivity() rather than requireContext().

// In an Activity:
val gmailClient = GmailClient(this, 4)

// In a Fragment:
val gmailClient = GmailClient(requireActivity(), 4)

The thread count determines how many threads process e-receipt emails in parallel. The SDK's internal default is 4.

Login

login() returns a Task<GoogleSignInAccount>. For safety, Google does not always silently sign the user in — it may require explicit authentication.

The SDK wraps Google's authentication handling into GmailAuthException. When this exception is thrown, it may contain an Android Intent provided by Google that must be launched via startActivityForResult(). Once the user selects an account, forward the result back to the SDK via onAccountAuthorizationActivityResult(), which also returns a Task<GoogleSignInAccount>.

note

The GmailAuthException flow is the most likely path on a user's first sign-in.

class GmailInboxFragment : Fragment() {

private lateinit var gmailClient: GmailClient

fun login() {
gmailClient.login()
.addOnSuccessListener { account: GoogleSignInAccount ->
// signed in successfully
}.addOnFailureListener { e ->
if (e is GmailAuthException) {
// Google requires explicit account selection from the user
startActivityForResult(e.signInIntent, e.requestCode)
} else {
// show error state
}
}
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)

gmailClient.onAccountAuthorizationActivityResult(requestCode, resultCode, data)
.addOnSuccessListener { account: GoogleSignInAccount ->
// account selected — signed in
}.addOnFailureListener {
// user cancelled, or an error occurred
}
}
}

Verify Existing Session

Use verify() to check for an already signed-in user without prompting them again. Returns Task<Boolean>.

  • true — a signed-in account exists; call credentials() to retrieve it silently.
  • false — no account found; take the user through the sign-in flow.
private fun verifyUser() {
gmailClient.verify()
.addOnSuccessListener { isLoggedIn ->
if (isLoggedIn) {
gmailClient.credentials()
.addOnSuccessListener { account: GoogleSignInAccount ->
// use the signed-in account
}
} else {
// prompt sign-in
}
}.addOnFailureListener { }
}

Logout

Signs the user out from Google and clears the cached date threshold used for email searching. Returns Task<Boolean> — always returns true on success; throws an exception on the rare failure.

After logout, a verify() call will return false and credentials() will throw an exception.

private fun logoutUser() {
gmailClient.logout().addOnSuccessListener {
// signed out and cache cleared
}
}

Configure and Read Messages

Configure the client before calling messages(). Returns Task<List<ScanResults>>.

PropertyTypeDefaultClient FunctionDescription
dayCutoffInt14dayCutoff(int days)Maximum number of days to look back in the inbox for receipts
filterSensitiveBooleanfalsefilterSensitive(boolean)When true, excludes sensitive/adult products from results
subProductsBooleanfalsesubProducts(boolean)Returns sub-products nested under parent items
countryCodeString"US"countryCode(String)Used for product intelligence classification
fun messages() {
client.messages(requireActivity())
.addOnSuccessListener { results: List<ScanResults> ->
// each item is a parsed receipt
}.addOnFailureListener { }
}

If no receipts are found, the list is empty. Each item in the list represents one successfully parsed receipt.

Destroy the Client

override fun onDestroy() {
super.onDestroy()
client.close()
}