Android SDK
The Kotlin library for Android apps, and what is in it today.
On this page
receviz-capture is the Kotlin library for Android apps: one Activity that finds the document in the camera, tells the person what to fix, takes the picture at the best moment, straightens and cleans it up and sends that one photo to ReceViz; and a coroutine client for apps that bring their own images.
Status#
In development: not yet run on a device or published
The library compiles and its unit tests pass, including a parity test against the rules it was ported from, but it has not yet run on a device or against the live API. It is not published to a Maven repository; expect changes before it is released.
Known limits today: guidance in English, suspend functions only (no callback API for Java callers of the client), no multi-page or PDF capture in the scanner (the client uploads PDFs), and an upload does not survive the app's process being ended.
Install#
Take it as a module of your build:
// settings.gradle.kts
include(":receviz-capture")
project(":receviz-capture").projectDir = file("../sdk/android/receviz-capture")
// app/build.gradle.kts
dependencies {
implementation(project(":receviz-capture"))
}minSdk 24, compileSdk 35 or newer, Java 17 to build. The library's manifest adds the camera and internet permissions and a scanner Activity; the camera is not required, so the app still installs on a device without one and the scanner offers a photo from the system picker, which needs no permission.
Scan and extract#
Configure ReceViz once, in Application.onCreate (Android can restore the scanner in a new process, and it finds its client there):
class App : Application() {
override fun onCreate() {
super.onCreate()
ReceViz.configure(this, ReceVizConfig(CredentialSource.TokenProvider { myApi.receVizToken() }))
}
}Launch the scanner, and use what comes back:
class ExpenseActivity : AppCompatActivity() {
private val scan = registerForActivityResult(ReceVizScanContract()) { result ->
if (result == null) return@registerForActivityResult // closed before a photo was taken
val extraction = result.extraction
if (extraction != null) {
val total = extraction.data["total"]?.asBigDecimal() // values, shaped like the document type
val date = extraction.fields["date"] as? FieldResult.Scalar // …each with its evidence
if (extraction.reviewRequired) showReview(extraction.review)
} else {
// Upload failed or the person closed the scanner while ReceViz was reading:
// result.error says why; result.imagePath still holds the photo.
}
}
fun onScanClicked() = scan.launch(ScanRequest(documentType = "payment_receipt", mode = ExtractionMode.VERIFIED))
}A ScanResult carries the extractionId, the parsed extraction, the imagePath of the photo in the app's private cache (deleted after a day: copy it if you keep it), its source (AUTO, SHUTTER or PHOTO_PICKER) and any error.
modeExtractionModeFAST,STANDARDorVERIFIED; by default the application's.schemaVersionIntA published version; by default the pinned or latest one.
metadatastringsYour own keys, returned on the extraction.
extractionOptionsdateOrder,tiling,includeOcr,evidence,referenceDate,defaultCurrency,knownMerchants,extra.optionsScannerOptionsautoCaptureandenhance(on),sendOnDeviceText(off),allowPhotoPickerandupload(on; off means capture only, and your server uploadsimagePath),nounandwaitTimeoutMs. Without anounthe scanner names the document from its type (“Point at an invoice”), then with the type's owncapture.nounonce the client has read it.
Credentials#
| Source | For | Notes |
|---|---|---|
CredentialSource.TokenProvider { … } | Apps with a server (recommended) | Called on first use, about 30 seconds before the token expires, and once more if ReceViz answers token_expired. Must return an rv_ct_ token. |
CredentialSource.PublishableKey("rv_pk_live_…") | Apps without a server | Exchanged for a client token at POST /v1/client-sessions, with the app's package name in X-ReceViz-Bundle-Id. Restrict the key to that package name in the console. |
CredentialSource.ClientToken("rv_ct_…") | Short tests | Cannot be renewed: calls fail with token_expired once it expires. |
A secret key is refused, and never sent: passed directly, it throws IllegalArgumentException; returned by a token provider, it fails with ReceVizException.Configuration. The base URL must be https, except for localhost, 127.0.0.1 or the emulator's 10.0.2.2. Credentials never appear in toString() or in logs. See client tokens for your server's half.
Use the client directly#
val client = ReceViz.client() // or ReceVizClient(config, bundleId = packageName)
val extraction = client.extract(jpeg, "image/jpeg", ExtractionRequest(
documentType = "payment_receipt",
options = ExtractionOptions(dateOrder = DateOrder.DMY),
idempotencyKey = UUID.randomUUID().toString(), // makes a retry after a network failure safe
))
client.extraction(id) // any state
client.waitForExtraction(id, timeoutMs = 60_000) // polls 1 s → 8 s, honours Retry-After
client.documentTypes()
client.me()Every call is a suspend function that does its I/O on Dispatchers.IO, with Android's own HttpURLConnection and org.json. Timeouts: 15 seconds to connect, 90 to read an upload, 20 otherwise. extract returns a finished extraction, polling a queued one, unless waitForResult = false. Every request sends X-ReceViz-SDK: android/0.1.0.
Failures are ReceVizExceptions with code, requestId, httpStatus and type: Authentication, Permission, InvalidRequest, RateLimited, Processing, Server, Network, Timeout, Cancelled and Configuration.
On-device text#
The scanner can also send the text read on the phone as client_ocr, using ML Kit's bundled Latin model (no Play services). It costs your app nothing unless you opt in, by adding the dependency and setting ScannerOptions(sendOnDeviceText = true):
implementation("com.google.mlkit:text-recognition:16.0.1")The model adds several megabytes per ABI to an APK; ship an App Bundle so each device downloads only its own. See Device text for what ReceViz does with it.
How the scanner decides#
About 8 frames a second are analysed, each reduced on the phone to a 96 × 128 brightness grid and a 24 × 32 edge-energy grid, far too coarse to read, then discarded. The rules are a Kotlin port of the same engine as the Web SDK, constant for constant, held to it by a parity test:
- It takes the picture when the frame is capturable, still for 300 ms, at 85% or more of the best in the last two seconds, never in the first 400 ms, with 1.2 s between attempts, and hands over to the shutter after three failed automatic attempts.
- Guidance names one issue at a time: light, distance, framing, tilt, glare, focus.
- The photo is cropped to the outline, straightened and kept at most 2,400 px on its long side, or 3,264 px for a long receipt, so ReceViz's Adaptive Document Tiling can read its small print.
- A failed upload offers Try again with the same photo and the same Idempotency-Key, so ReceViz returns the first attempt's extraction rather than starting another.
Privacy#
- Camera frames are analysed on the phone, as averages, and discarded. Nothing is sent while the camera runs.
- One upload per scan: the photo the person kept, and with
sendOnDeviceTextthe text read from that same photo on the device. - The photo is written to the app's private cache, which the SDK clears of anything older than a day.
- Quality feedback carries per-field outcomes only, never a value or a line of text.
- The SDK logs error codes and request ids, never credentials or document content.