Overview
Address verification passively confirms where a customer sleeps by analyzing overnight location data collected by the Uselira mobile SDK. No manual check or separate customer interaction beyond the initial setup is required.
The SDK creates the verification job, uploads location events in the background, and exposes the resulting jobId to your app. Your server then reads the final verdict via the Lira API or receives it via webhook.
How it works
sequenceDiagram
participant App as Mobile app
participant Lira
participant Server as Your server
App->>Lira: Uselira.launchSetup
Lira-->>App: Customer confirms address
Lira-->>App: onSetupComplete(jobId)
Note over App: SDK tracks location nightly
App->>Lira: SDK uploads location events
Server->>Lira: GET /address-verification/jobs/{jobId}
Lira-->>Server: Verdict
Three actors:
- The Uselira mobile SDK (Android, iOS, React Native) runs inside your app, walks the customer through address confirmation, then tracks location during the configured nightly window. The SDK creates the verification job, uploads events, and surfaces a
jobIdto your code. - Your server receives the
jobIdfrom the mobile client after setup completes, then pollsGET /address-verification/jobs/{jobId}until the job reaches a terminal status. - Lira scores each qualifying night, accumulates evidence, and closes the job with a verdict once enough data has arrived.
1. Install the SDK
| Platform | Coordinate | Registry |
|---|---|---|
| Android | com.uselira.sdk:core | Maven Central |
| iOS | UseliraCore | SPM + CocoaPods |
| React Native | @uselira/core | npm |
yarn add @uselira/core
cd ios && pod installMinimum versions: Android API 26, iOS 16.0, Swift 5.9, Kotlin 1.9, React Native 0.73.
2. Permissions
The SDK guides the customer through the location consent screens automatically. You must add the required permission entries to your project first.
<key>NSLocationWhenInUseUsageDescription</key>
<string>Lira needs your location to confirm your residential address.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Lira needs continuous access to your location to verify your address over the required period.</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>processing</string>
<string>fetch</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.uselira.sdk.upload</string>
<string>com.uselira.sdk.watchdog</string>
</array>Warning
The BGTaskSchedulerPermittedIdentifiers entries are required. Uselira.restoreIfNeeded() registers handlers for both IDs and iOS crashes the host app on launch if they aren't declared.
3. Launch the setup flow
Call Uselira.launchSetup when you're ready to verify a customer. The SDK presents a full-screen native UI that confirms the address on a map and acquires location permissions. The host receives a jobId once setup completes, or a cancellation / error otherwise.
import { launchSetup } from '@uselira/core';
const result = await launchSetup(
{
apiKey: 'YOUR_API_KEY',
customerRef: 'CUSTOMER_UNIQUE_ID', // your stable ID for this customer
duration: 14, // REQUIRED, nights to observe, 3–90
environment: 'sandbox', // REQUIRED, 'sandbox' or 'production'
scheduleStart: '20:00', // start of nightly window (HH:MM)
scheduleEnd: '06:00', // end of nightly window
},
{
theme: { brand: { accent: '#1565C0' } }, // optional branding
},
);
if (result?.jobId) {
// Send result.jobId to your server to poll for the verdict later
}Note
The React Native module currently exposes launchSetup, stop, and addLocationListener only. cancelVerification, status, and restoreIfNeeded are iOS/Android-native APIs. Call them from your native code if you need them on RN.
Note
Job creation and event upload happen inside the SDK using your API key. There is no public REST endpoint for creating verification jobs. The SDK is the only supported client.
4. Read the verdict (server-side)
Once your server has the jobId, poll GET /api/v1/address-verification/jobs/{jobId} with your API key until the job reaches a terminal status.
curl https://api.uselira.com/api/v1/address-verification/jobs/JOB_ID \
-H "X-API-Key: YOUR_API_KEY"Response: terminal verdict
{
"job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"customer_ref": "CUSTOMER-001",
"status": "complete",
"confidence_score": 82.4,
"verdict": "high",
"observation_days": 14,
"tracking_window": "20:00–06:00",
"radius_meters": 100,
"summary": {
"observation_days": 14,
"nights_observed": 12,
"nights_at_home": 11,
"nights_unobserved_power": 1,
"nights_unobserved_app_closed": 1,
"avg_hours_at_home": 7.3,
"tracking_window": "20:00–06:00",
"radius_meters_used": 100
},
"claimed_location": {
"lat": -15.416,
"lng": 28.283,
"label": "Home"
},
"country_code": "ZM",
"created_at": "2026-04-01T10:00:00.000Z",
"completed_at": "2026-04-15T06:00:00.000Z"
}Response: while tracking
{
"job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "tracking",
"confidence_score": null,
"verdict": null,
"preliminary_score": 68,
"confidence_band": "medium",
"nights_remaining": 2,
"summary": null,
"...": "..."
}Job statuses
| Status | Description |
|---|---|
pending | Job created, waiting for the first location event from the SDK. |
tracking | Location events are arriving and being scored nightly. preliminary_score, confidence_band, and nights_remaining are set. |
complete | Sufficient nights observed. verdict and confidence_score are set. |
inconclusive | Job closed without enough data (full period elapsed or extended silence). |
cancelled | Cancelled via the SDK or via DELETE /address-verification/jobs/{jobId}. |
Verdict values (when status is complete)
| Verdict | Meaning |
|---|---|
high | Strong evidence the customer's nightly location matches the claimed address. |
moderate | Partial evidence: some nights at the address, some away. |
low | Little or no evidence of presence at the claimed address. |
inconclusive | Verdict could not be determined, typically paired with status: "inconclusive". |
List all jobs
curl https://api.uselira.com/api/v1/address-verification/jobs \
-H "X-API-Key: YOUR_API_KEY"Returns { "jobs": [ ... ] } for the authenticated organization.
5. Receive results via webhook
Info
Coming soon. Webhook delivery for address-verification jobs is on the roadmap but not yet enabled. For now, poll GET /address-verification/jobs/{jobId} (see section 4). When webhook support ships, the events will be exposed via the same /api/v1/client/webhooks endpoint used by the rest of the Lira API.
6. Cancel a job
A job can be cancelled in two ways:
From the mobile app (recommended for "delete my data" flows)
let result = await Uselira.cancelVerification(jobId: jobId, apiKey: apiKey)
// On success, local tracking is also stopped automatically.val result = Uselira.cancelVerification(context, jobId, apiKey)
// On success, local tracking is also stopped automatically.The SDK treats HTTP 204 and 404 equivalently as success. The job is gone either way.
From your server
curl -X DELETE https://api.uselira.com/api/v1/address-verification/jobs/JOB_ID \
-H "X-API-Key: YOUR_API_KEY"Returns 204 No Content on success. Only jobs in pending or tracking state can be cancelled.
Note
Cancelling from your server stops backend scoring but does not stop the SDK's on-device tracking. For a complete teardown, also call Uselira.stop() from your mobile app, or use the SDK's cancelVerification helper instead. It does both.
7. Check verification status on the device
Read the current verification state at any time without making a network call:
let status = Uselira.status
print("Active: \(status.isActive), night \(status.nightsCompleted) of \(status.duration)")val status = Uselira.status(context)
// status.isActive, status.jobId, status.nightsCompleted, status.duration, status.nightsRemainingUseful for rendering host UI like "Verifying, night 3 of 14" or gating re-onboarding flows.
8. Best practice: don't start a second verification while one is in flight
A verification is a multi-night, billable job. Calling launchSetup again while a previous job is still pending or tracking creates a second billable job on the backend: duplicate billing, duplicate webhooks, two competing tracking sessions on-device.
The SDK does include a defence in depth: it sends a deterministic Idempotency-Key derived from (customerRef, lat, lng, consent-minute), so a user double-tapping Save & continue in the same minute collapses to a single job server-side. But that's a backstop, not the primary gate. Hosts should gate the entry point themselves.
The right shape is:
- Persist the
jobIdreturned bylaunchSetupto durable storage (AsyncStorage / Keychain on iOS, EncryptedSharedPreferences on Android). The SDK restores its own native tracking state on app launch (Uselira.restoreIfNeeded()on iOS,Uselira.start(context)on Android), but the host is responsible for remembering thejobIdfor UI gating and status polling. - On every app launch, read the stored
jobIdand call the status helper from section 7 (Uselira.status/Uselira.status(context)). The SDK knows whether tracking is active for that job. - Hide or disable the Start Verification entry point while
status.isActiveis true. Show progress (e.g. "Verifying, night 3 of 7") instead. - Clear the stored
jobIdwhen the job reaches a terminal state (complete,inconclusive, orcancelled) or when the user explicitly cancels. PollGET /address-verification/jobs/{jobId}or listen for the webhook to detect terminal states.
import { launchSetup, stop, addSetupFailedListener } from '@uselira/core';
import AsyncStorage from '@react-native-async-storage/async-storage';
const STORAGE_KEY = 'lira.activeJobId';
function HomeScreen() {
const [activeJobId, setActiveJobId] = useState<string | null>(null);
// Restore on mount.
useEffect(() => {
AsyncStorage.getItem(STORAGE_KEY).then(setActiveJobId);
}, []);
const handleStart = async () => {
if (activeJobId) return; // already verifying, show progress, not the CTA
const result = await launchSetup({ /* …config… */ }, { /* …ui… */ });
if (result?.jobId) {
setActiveJobId(result.jobId);
await AsyncStorage.setItem(STORAGE_KEY, result.jobId);
}
};
const clearJob = async () => {
await AsyncStorage.removeItem(STORAGE_KEY);
setActiveJobId(null);
};
return activeJobId
? <VerificationInProgress jobId={activeJobId} onCleared={clearJob} />
: <Button title="Verify my address" onPress={handleStart} />;
}Note
The React Native example app in @uselira/core demonstrates the in-memory version of this gate. Add persistence with AsyncStorage / Keychain / EncryptedSharedPreferences in production.
9. Error handling
onSetupFailed (delegate / listener) and cancelVerification failures both surface a LiraSetupError:
| Case | When it fires | Host action |
|---|---|---|
.network | Offline, DNS, TLS, timeout. | Show a connectivity error. Retry is sensible. |
.unauthorized | HTTP 401. API key is wrong or revoked. | Configuration error. Do not retry. |
.rateLimited(retryAfterSeconds:) | HTTP 429. Retry-After parsed if present. | Wait the indicated seconds before retrying. The SDK respects this internally and short-circuits event uploads until the window passes. |
.server(statusCode:body:detail:) | Other 4xx/5xx. body is the truncated raw response; detail is a parsed ServerErrorDetail with the Lira error envelope when present (code, message, actionType, actionHint). | Treat as transient and retry with backoff, unless detail.actionType says otherwise. FIX_INPUT and CONTACT_ADMIN won't be resolved by a retry. Show detail.actionHint to the user when present; the SDK's own banner already does. |
.invalidConfig(reason:) | SDK detected a problem before reaching the backend. | Programming error or backend regression. Log and report. |
The SDK keeps the user on the address review screen with a retry-able banner; onSetupFailed lets the host additionally respond (toast, dismiss, route to a config screen, log to Sentry).
React Native, subscribe via addSetupFailedListener:
import { addSetupFailedListener, type SetupFailure } from '@uselira/core';
const sub = addSetupFailedListener((failure: SetupFailure) => {
if (failure.kind === 'server' && failure.actionHint) {
Toast.show(failure.actionHint);
}
});
// later: sub.remove()The RN bridge also exposes addUploadAttemptedListener for batch-upload outcomes ({ success, count, error? }), useful for a "samples uploaded" indicator or detecting persistent upload failures.
10. Configuration reference
| Field | Type | Default | Description |
|---|---|---|---|
apiKey | string | required | Your Lira API key from the dashboard. |
customerRef | string | required | Your stable identifier for this customer (e.g. user ID). |
environment | enum | required | Which Uselira API the SDK talks to. iOS: .sandbox / .production. Android: LiraEnvironment.SANDBOX / LiraEnvironment.PRODUCTION. React Native: 'sandbox' / 'production'. No default. The host app must pick, so a sandbox build cannot accidentally ship to production. |
duration | int | required | Nights to track, 3–90, before auto-stopping. No default. You must choose. |
scheduleStart | string | "20:00" | Start of nightly tracking window, HH:MM 24-hour. |
scheduleEnd | string | "06:00" | End of the window. If earlier than scheduleStart, spans midnight. |
batteryThreshold | int | 15 | Battery % (1–99) below which sampling pauses. Resumes when battery recovers. |
sampleInterval | int | 15 min | How often to request a location fix. iOS: seconds; Android & RN: milliseconds. Minimum: 10 s. |
syncInterval | int | 15 min | How often to upload buffered events. iOS: seconds; Android & RN: milliseconds. Minimum: 10 s. |
UiConfig controls the appearance of the native setup screens (button colors, copy, map style). See the iOS SDK distribution for the full theming API.
11. Behavior guarantees
A few SDK-internal behaviors worth knowing:
- Hard cap on indefinite tracking. If event uploads fail chronically, the SDK enforces a wall-clock cap at
2 × durationdays, then callsstop()itself. Retry-Afteris honoured. On HTTP 429 the SDK sets an internal throttle window; subsequent uploads short-circuit (no network call) until the window passes.- Idempotent setup. The create-job call uses an idempotency key derived from
(customerRef, lat, lng, consent-minute UTC). Same-minute retries of the same address collapse to a single backend job, protecting against double-tap duplicate billing. Cross-flow retries (after the user dismisses and relaunches the SDK) get a fresh key, so they reach the validation layer instead of replaying a cached failure. - Listener threading. All callbacks (
onSetupComplete,onSetupCancelled,onSetupFailed,onLocationRecorded,onUploadAttempted) are invoked on the main thread. - Android listener leak safety.
Uselira.trackingListeneris held as aWeakReference. Passingthisfrom an activity / fragment is safe.