Skip to content
Lira APILira API

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:

  1. 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 jobId to your code.
  2. Your server receives the jobId from the mobile client after setup completes, then polls GET /address-verification/jobs/{jobId} until the job reaches a terminal status.
  3. Lira scores each qualifying night, accumulates evidence, and closes the job with a verdict once enough data has arrived.

1. Install the SDK

PlatformCoordinateRegistry
Androidcom.uselira.sdk:coreMaven Central
iOSUseliraCoreSPM + CocoaPods
React Native@uselira/corenpm
Terminal
yarn add @uselira/core
cd ios && pod install

Minimum 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.

XML
<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.

TypeScript
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.

Terminal
curl https://api.uselira.com/api/v1/address-verification/jobs/JOB_ID \
  -H "X-API-Key: YOUR_API_KEY"

Response: terminal verdict

JSON
{
  "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

JSON
{
  "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

StatusDescription
pendingJob created, waiting for the first location event from the SDK.
trackingLocation events are arriving and being scored nightly. preliminary_score, confidence_band, and nights_remaining are set.
completeSufficient nights observed. verdict and confidence_score are set.
inconclusiveJob closed without enough data (full period elapsed or extended silence).
cancelledCancelled via the SDK or via DELETE /address-verification/jobs/{jobId}.

Verdict values (when status is complete)

VerdictMeaning
highStrong evidence the customer's nightly location matches the claimed address.
moderatePartial evidence: some nights at the address, some away.
lowLittle or no evidence of presence at the claimed address.
inconclusiveVerdict could not be determined, typically paired with status: "inconclusive".

List all jobs

Terminal
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:

Swift
let result = await Uselira.cancelVerification(jobId: jobId, apiKey: apiKey)
// On success, local tracking is also stopped automatically.
Kotlin
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

Terminal
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:

Swift
let status = Uselira.status
print("Active: \(status.isActive), night \(status.nightsCompleted) of \(status.duration)")
Kotlin
val status = Uselira.status(context)
// status.isActive, status.jobId, status.nightsCompleted, status.duration, status.nightsRemaining

Useful 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:

  1. Persist the jobId returned by launchSetup to 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 the jobId for UI gating and status polling.
  2. On every app launch, read the stored jobId and call the status helper from section 7 (Uselira.status / Uselira.status(context)). The SDK knows whether tracking is active for that job.
  3. Hide or disable the Start Verification entry point while status.isActive is true. Show progress (e.g. "Verifying, night 3 of 7") instead.
  4. Clear the stored jobId when the job reaches a terminal state (complete, inconclusive, or cancelled) or when the user explicitly cancels. Poll GET /address-verification/jobs/{jobId} or listen for the webhook to detect terminal states.
TypeScript
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:

CaseWhen it firesHost action
.networkOffline, DNS, TLS, timeout.Show a connectivity error. Retry is sensible.
.unauthorizedHTTP 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:

TypeScript
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

FieldTypeDefaultDescription
apiKeystringrequiredYour Lira API key from the dashboard.
customerRefstringrequiredYour stable identifier for this customer (e.g. user ID).
environmentenumrequiredWhich 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.
durationintrequiredNights to track, 3–90, before auto-stopping. No default. You must choose.
scheduleStartstring"20:00"Start of nightly tracking window, HH:MM 24-hour.
scheduleEndstring"06:00"End of the window. If earlier than scheduleStart, spans midnight.
batteryThresholdint15Battery % (1–99) below which sampling pauses. Resumes when battery recovers.
sampleIntervalint15 minHow often to request a location fix. iOS: seconds; Android & RN: milliseconds. Minimum: 10 s.
syncIntervalint15 minHow 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 × duration days, then calls stop() itself.
  • Retry-After is 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.trackingListener is held as a WeakReference. Passing this from an activity / fragment is safe.