# Android SDK

The Android SDK (`com.didww.android.sdk.verification`) is an on-device Kotlin client for the
[Verification API](../api-reference/index.html#otp-verification-api-reference). It supports SMS and phone call
through a coroutine-based API, represents each verification as a `Flow` of states, and
provides typed errors that your app can handle.

- **Source:** [didww/didww-verification-android-sdk](https://github.com/didww/didww-verification-android-sdk)
- **Requires:** Android `minSdk 23`.
- **Build from source:** Android `compileSdk 36`, JDK `17`, and Kotlin `2.4.10`.
- **Dependencies:** Kotlin coroutines and `kotlinx.serialization`.
- **Transport:** Uses the Android platform HTTP stack. There is no third-party HTTP client to
  configure.
- **Permissions:** Declares `INTERNET` directly and does not require a dangerous runtime
  permission.
- **Sensitive data:** Does not persist or write OTP codes, phone numbers, or credentials to
  diagnostic logs.

Note

Mobile app credentials can be extracted from the app binary. For that reason, the
on-device SDK supports only the `public` and `basic` authentication schemes.
HMAC-signed server-to-server authentication is not available because it requires a
signing secret. See [Authentication](../authentication.html#otp-verification-authentication).

The SDK is written for Kotlin. Coroutines, `Flow`, and default arguments are part of its
public API, so Java is not a supported integration path.

## Before you begin

- Create an OTP application and obtain its credentials in the environment where the SDK will
  send verification requests. See
  [Getting Started](../getting-started.html#otp-verification-getting-started).
- Set the OTP application's **Callback URL** to an endpoint on your backend. Configure that
  endpoint to verify callback signatures using the application secret and return `allow` or
  `deny`. See [Callbacks](../callbacks.html#otp-verification-callbacks).
- Keep the OTP application's minimum authentication mode set to `public` so requests made
  with `Auth.Public` are accepted.
- Copy the application key into the Android app and use it with `Auth.Public`. Keep the
  application secret on your backend; do not include it in the APK.
- Choose the [verification methods](../verification-methods/index.html#otp-verification-methods) your app will support and
  add the matching SDK artifact. Use `verification-all` to select methods at runtime,
  `verification-sms` for SMS only, or `verification-core` for phone call only. See
  [Installation](#installation).
- Collect each handle's `states` flow exactly once from a lifecycle-aware scope such as
  `viewModelScope`. The first collection sends the verification request. See
  [Collect once](#collect-once).
- Provide manual code entry for every supported method. Automatic SMS capture requires Google
  Play Services and should supplement, rather than replace, manual entry. See
  [Automatic SMS capture](#automatic-sms-capture).

---

## How a verification flows

The SDK models a verification as a **state machine** rather than a sequence of separate
requests. Your app starts a handle, collects its states, and submits the user's code. Each
outcome, including success, failure, denial, and expiry, is returned as a state.

```
        %%{init: {
  "theme": "base",
  "themeVariables": {
    "primaryColor": "#e0f2fe",
    "primaryBorderColor": "#38bdf8",
    "primaryTextColor": "#1f2d3d",
    "lineColor": "#38bdf8",
    "secondaryColor": "#ccfbf1",
    "tertiaryColor": "#fef3c7",
    "fontSize": "14px"
  }
}}%%

stateDiagram-v2
    [*] --> Starting: First collection starts create or lookup

    Starting --> AwaitingInput
    Starting --> Denied
    Starting --> SetupError
    Starting --> Failed

    AwaitingInput --> Submitting: Submit a value
    AwaitingInput --> Captured: SMS code captured</br>automatically
    Captured --> Submitting

    Submitting --> Verified
    Submitting --> AwaitingInput: Rejected, lastError set
    Submitting --> Failed
    Submitting --> Expired

    Verified --> [*]
    Failed --> [*]
    Denied --> [*]
    SetupError --> [*]
    Expired --> [*]
```

Your app does not verify the code itself. It collects the code from the user and passes it to
the SDK. The SDK handles the network requests and state transitions, while your app owns the
user interface.

If the user submits an incorrect code, the verification can remain open for another attempt.
The flow returns to `AwaitingInput` with `lastError` set, so your app can show the error
while still accepting a new value. If the API returns `too_many_attempts`, the verification
becomes terminal.

### States

| State | Meaning | Terminal |
| --- | --- | --- |
| `Starting` | The create request from `start(...)` or the by-number lookup from `resume(...)` is in flight. |  |
| `AwaitingInput` | The verification is waiting for a code. Carries `verificationId` and may also provide `deliveryMethod`, `destination`, `fee`, `expiresAtEpochMillis`, `sms`, and `lastError`. |  |
| `Captured` | An SMS code was captured automatically and is about to be submitted. See [Automatic SMS capture](#automatic-sms-capture). |  |
| `Submitting` | A value is in flight. |  |
| `Verified` | The server accepted the value. | Yes |
| `Failed` | Carries a `FailureReason` - either an API error or an SDK-side one. | Yes |
| `Denied` | The application's callback rejected the request or did not return a usable response. | Yes |
| `SetupError` | The OTP application is misconfigured. Retrying or changing user input cannot resolve it. | Yes |
| `Expired` | The deadline passed with no accepted value. | Yes |

Note

The `fee` value on `AwaitingInput` is the quoted verification fee, not an immediate
charge. It is billed only when the verification reaches `Verified`. The quote does not
include the SMS or call used to deliver the challenge, which is billed separately.

## Installation

The SDK is published as three artifacts. Choose the artifact that matches the delivery methods
your app uses. All artifacts use the `com.didww.android.sdk.verification` group ID and are
published to Maven Central.

| Artifact | Contains | Depend on it when |
| --- | --- | --- |
| `verification-core` | Transport, error model, state machine, and phone call. | You only use phone call. |
| `verification-sms` | `verification-core` plus the SMS channel and Google Play Services integration. | You only send SMS. |
| `verification-all` | The umbrella artifact, including `DidwwVerification`. | You choose the channel at runtime. |

```
// settings.gradle.kts - repositories { mavenCentral() }

dependencies {
    implementation("com.didww.android.sdk.verification:verification-all:1.1.0")
}
```

Note

`verification-core` directly declares only `INTERNET`. `verification-sms` also brings
in Google Play Services and related AndroidX manifest components through its dependencies,
but it does not require a dangerous runtime permission. Apps that do not use SMS can depend
on `verification-core` to avoid those SMS-related dependencies. See the
[measured manifest breakdown](https://github.com/didww/didww-verification-android-sdk/blob/main/docs/manifest-cost.md).

## Quick start

The following example keeps the SDK client and verification handle in a `ViewModel`. It
starts an SMS verification, collects the handle exactly once, and exposes the current state to
the UI through a `StateFlow`:

```
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.didww.android.sdk.verification.Auth
import com.didww.android.sdk.verification.DeliveryMethod
import com.didww.android.sdk.verification.Environment
import com.didww.android.sdk.verification.VerificationHandle
import com.didww.android.sdk.verification.VerificationState
import com.didww.android.sdk.verification.all.DidwwVerification
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class VerifyViewModel(application: Application) : AndroidViewModel(application) {

    private val didww = DidwwVerification(
        context = application,
        auth = Auth.Public(BuildConfig.DIDWW_APPLICATION_KEY),
        environment = Environment.Sandbox,
    )

    private val _state = MutableStateFlow<VerificationState?>(null)
    val state: StateFlow<VerificationState?> = _state.asStateFlow()

    private var handle: VerificationHandle? = null

    fun start(destination: String) {
        val started = didww.start(destination, DeliveryMethod.SMS)
        handle = started
        _state.value = null

        // Collect each handle once, outside the view layer.
        viewModelScope.launch {
            started.states.collect { emission ->
                // An older handle must not overwrite the state of a newer start.
                if (started === handle) _state.value = emission
            }
        }
    }

    fun submit(value: String) {
        handle?.submit(value)
    }
}
```

Render the `StateFlow` with lifecycle awareness. The UI functions below are placeholders for
your own components:

```
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle

@Composable
fun VerifyScreen(viewModel: VerifyViewModel) {
    val state by viewModel.state.collectAsStateWithLifecycle()

    when (val current = state) {
        null -> PhoneNumberEntry(onStart = viewModel::start)
        VerificationState.Starting -> Spinner("Requesting a code")
        is VerificationState.AwaitingInput -> CodeEntry(
            hint = current.sms?.template,
            error = current.lastError?.detail,
            expiresAt = current.expiresAtEpochMillis,
            onSubmit = viewModel::submit,
        )
        is VerificationState.Captured -> Spinner("Code received")
        VerificationState.Submitting -> Spinner("Checking")
        is VerificationState.Verified -> Success()
        is VerificationState.Failed -> Failure(current.reason)
        is VerificationState.Denied -> Failure(current.error?.detail)
        is VerificationState.SetupError -> Misconfigured(current.code, current.detail)
        VerificationState.Expired -> Expired()
    }
}
```

Calling `start()` does not send the request. It only creates the handle. The request starts
when your app first collects `handle.states`. This example uses the sandbox; switch to
`Environment.Production` and production credentials for live verifications.

The SDK retains `context.applicationContext` internally. Passing an `Activity` as the
constructor context does not cause the SDK to retain that `Activity`.

### Collect once

Warning

`handle.states` is a cold `Flow` and can be collected only once. If it is collected a
second time, the handle emits `Failed(FailureReason.Sdk(SdkError.AlreadyRunning))`. After
that, the same handle cannot be used again.

Collect `handle.states` from a `ViewModel`-scoped coroutine and mirror the states into a
`StateFlow` for the UI to render. Do not collect directly from a composable or an
`Activity` that can be recreated.

This keeps lifecycle events, such as screen rotation, from attempting to collect the same
handle again. Reuse the same `DidwwVerification` instance across starts so its in-process
supersession tracking also remains active.

The first collection starts the request. Cancelling the collection cancels the in-flight
request and releases resources registered by the channel. There is no separate `stop()`
method. To retry, start a new handle.

### Submitting a value

The handle can receive submitted values as soon as it is created. If the user submits a value
before `AwaitingInput` is emitted, the SDK buffers it instead of dropping it.

`submit()` never blocks and never throws, so your UI does not need to wait for a network
round trip to call it. Disable repeated submission while the state is `Submitting`. Every
submitted value is queued, so duplicate taps can produce additional report attempts.

A submitted value can be accepted only after the verification has been dispatched, such as
after the SMS is sent or the call is placed. If a value is submitted too early, the API returns
`ApiErrorCode.NOT_READY_TO_REPORT` (`not_ready_to_report`). This is retryable: the state
machine returns to `AwaitingInput` with `lastError` set. The value can be submitted again
after the verification is ready.

### Expiry

`AwaitingInput.expiresAtEpochMillis` is the server-provided expiry time for the
verification. The SDK does not define its own TTL.

The local countdown uses elapsed time instead of the device wall clock, so time changes on the
device, such as an NTP correction or a manual date change, do not shorten the verification
window.

The SDK can emit `Expired` from this local countdown, but the server remains authoritative. A
late submitted value is still sent to the API, and `Expired` is not emitted while a submission
is in flight.

## Delivery methods

`DidwwVerification` selects the delivery method at runtime:

```
didww.start(number, DeliveryMethod.SMS)     // a code by text message
didww.start(number, DeliveryMethod.CALLOUT) // a spoken code
```

If your app uses only one delivery method, you can use its channel-specific class directly:

```
import com.didww.android.sdk.verification.sms.SmsVerification          // verification-sms
import com.didww.android.sdk.verification.callout.CalloutVerification  // verification-core

SmsVerification(context, auth).start(number)
CalloutVerification(context, auth).start(number)
```

For both `SMS` and `CALLOUT`, the user submits the code they received.

### Delivery-method options

Method-specific options use the same name as the delivery method. For example, SMS options are
passed with `sms` in Kotlin and sent as an `sms` block in the API request.

```
didww.start(number, DeliveryMethod.SMS, sms = SmsOptions(languages = listOf("en-US")))
```

The SDK also computes the Android SMS Retriever app hash when it is available. The resulting
request has this form:

```
{
  "data": {
    "destination": "+4915112345678",
    "delivery_method": "sms",
    "sms": {
      "languages": ["en-US"],
      "app_hash": "<computed-app-hash>"
    }
  }
}
```

`CALLOUT` takes the announcement language the same way:

```
didww.start(number, DeliveryMethod.CALLOUT, callout = CalloutOptions(languages = listOf("de-DE")))
```

`SmsOptions.languages` and `CalloutOptions.languages` accept the same tags with the same
semantics, so one language list works for both delivery methods. The catalogues behind them
differ, however: the announcement recordings are a different set from the message templates,
so a tag that is honored for SMS can still fall back for a phone call. See
[SMS languages](../verification-methods/sms.html#otp-verification-sms-languages) and
[phone call languages](../verification-methods/phone-call.html#otp-verification-callout-languages).

Passing options for a different channel throws `IllegalArgumentException` before the request
is sent:

```
didww.start(number, DeliveryMethod.CALLOUT, sms = SmsOptions(...))   // throws
```

The API reads only the options block that matches `delivery_method`. The SDK rejects
mismatched options early so the app does not start a verification with unintended defaults.

In the response, `AwaitingInput.sms` contains SMS-specific details returned by the API:
`template` (the message with `{{CODE}}` still in it), `language` (the template language
the API selected), `interceptionTimeoutSeconds`, and `codeLength`.
`AwaitingInput.callout` contains `language`, the language the announcement was played in,
and `codeLength`. Both `language` values report what the API **selected**, which is not
necessarily the first language requested. `codeLength` is the length of this verification's
code: 4 to 8 digits, set per application (6 by default).

## Resume after process recreation

A `VerificationHandle` exists only in memory. If Android terminates your app process after a
verification starts, persist the destination and use `resume(...)` to reattach to the latest
verification for that number. Do not call `start(...)` to recover an existing verification;
starting another one supersedes the previous verification and sends a new challenge.

Like `start(...)`, `resume(...)` performs no I/O until the returned handle's `states`
flow is collected:

```
fun resume(destination: String) {
    val resumed = didww.resume(destination, DeliveryMethod.SMS)
    handle = resumed
    _state.value = null

    viewModelScope.launch {
        resumed.states.collect { emission ->
            if (resumed === handle) _state.value = emission
        }
    }
}
```

The SDK removes non-digit characters from the destination when it builds the by-number path,
so formatted and unformatted versions of the same number reach the same endpoint. A value with
no digits throws `IllegalArgumentException` before a request is sent.

The resumed handle behaves according to the verification found by the API:

| API result | State emitted by the handle |
| --- | --- |
| The latest verification is active | `AwaitingInput`. The app can submit the code through the resumed handle. |
| The latest verification is finished | Its terminal state, such as `Verified`, `Failed`, `Denied`, or `Expired`. |
| No verification exists for the number | `Failed` with an API error whose `known` value is `ApiErrorCode.NOT_FOUND`. |

The `method` argument selects the channel-specific client behavior. For SMS, it enables the
automatic capture checks. The delivery method returned by the API remains authoritative and is
used when the SDK reports a submitted value.

See [Get verification status by
number](../api-reference/get-verification-status-by-number.html#otp-verification-api-status-by-number) and [Report a
verification by number](../api-reference/report-verification-by-number.html#otp-verification-api-report-by-number) for
the underlying API behavior.

## Environments

Choose the environment when creating the SDK client. If no environment is provided, the SDK
uses `Environment.Production`. The SDK removes a trailing slash from the selected base URL
and appends `/api/v1`.

| Environment | Host |
| --- | --- |
| `Environment.Production` (default) | `https://verification.didww.com` |
| `Environment.Sandbox` | `https://verification-sandbox.didww.com` |
| `Environment.Custom(url)` | A custom scheme and host, optionally with a base path, for a local backend, proxy, or test server. |

Note

Use `Environment.Sandbox` while building and testing your integration.

Use credentials from an OTP application created in the same environment as the SDK client.
See [Choose an environment](../getting-started.html#otp-verification-environments).

```
DidwwVerification(context, auth, Environment.Sandbox)
DidwwVerification(context, auth, Environment.Custom("http://10.0.2.2:3000"))
```

Timeouts are configured through `Config`. See [Cancellation and timeouts](#cancellation-and-timeouts).

## Authentication

| Scheme | Header | Use |
| --- | --- | --- |
| `Auth.Public(applicationKey)` | `Application <key>` | Production, on-device. |
| `Auth.Basic(key, secret)` | `Basic base64(key:secret)` | Local development only. |

Use `Auth.Public` for production mobile apps. The application key identifies your
application, but it does not by itself authorize a verification. DIDWW asks your application's
callback URL to approve each request before sending an SMS or phone call. This corresponds to
the API's [public auth mode](../authentication.html#otp-verification-authentication).

Warning

`Auth.Basic` uses a server-to-server secret. Do not include this secret in a production
mobile app, because it can be recovered from the APK. The SDK logs a warning at runtime if
`Auth.Basic` is used in a build that is not marked debuggable. If this secret has already
been shipped in an app, treat it as disclosed and rotate it.

Keep the OTP application's **minimum auth mode** set to `public` when it is used by the
Android SDK. Raising the minimum auth mode rejects SDK requests: `basic` requires a secret
in the APK, and signed authentication also requires a signing secret on the device. Use a
stricter minimum auth mode only for applications driven by your own server.

## A start can come back denied

With `Auth.Public`, DIDWW asks your application's [request callback](../callbacks.html#otp-verification-callbacks) to
authorize the start request before sending an SMS or phone
call. If the callback denies the request, the HTTP request can still succeed with
`201 Created` and the flow reaches `Denied` rather than `AwaitingInput`.

```
        %%{init: {
  "theme": "base",
  "themeVariables": {
    "actorBkg": "#e0f2fe",
    "actorBorder": "#38bdf8",
    "actorTextColor": "#1f2d3d",
    "actorLineColor": "#38bdf8",

    "signalColor": "#1f2d3d",
    "signalTextColor": "#1f2d3d",

    "noteBkgColor": "#fef3c7",
    "noteBorderColor": "#facc15",
    "noteTextColor": "#1f2d3d",

    "labelBoxBkgColor": "#ccfbf1",
    "labelBoxBorderColor": "#2dd4bf",
    "labelTextColor": "#1f2d3d",
    "loopTextColor": "#1f2d3d"
  }
}}%%

sequenceDiagram
    participant SDK as Verification SDK
    participant API as Verification API
    participant CB as Your backend

    SDK->>API: POST /api/v1/verifications
    API->>CB: POST {callback_url}<br/>Authorization: Application {appKey}:{signature}<br/>x-timestamp: {unix seconds}
    Note over CB: verify the signature,<br/>then decide

    alt your backend allows
        CB-->>API: 200 {"action":"allow"}
        API-->>SDK: 201 Created - AwaitingInput
    else your backend denies
        CB-->>API: 200 {"action":"deny"}
        API-->>SDK: 201 Created - Denied<br/>denied_by_callback
    else no usable answer
        CB--xAPI: non-2xx, timeout, invalid JSON,<br/>unknown action, or a body over 8 KB
        API-->>SDK: 201 Created - Denied<br/>denied_invalid_callback_response
    end

    Note over SDK,CB: No challenge is dispatched in either denied branch.
```

### SetupError means a configuration problem

`SetupError` indicates a configuration problem that the user cannot fix. With
`Auth.Public`, this happens when the application has no callback URL configured. In that
case, each start request is denied with `denied_missing_callback_url` until a callback URL
is added.

```
is VerificationState.SetupError ->
    // Retrying will not help; no user input can rescue it.
    Log.e("didww", "verification misconfigured: ${state.code} ${state.detail}")
```

Treat `SetupError` as an application configuration issue, not as a phone number problem.
Log it or report it to your team, and avoid showing it to the end user as a retryable
verification failure. See [Callbacks](../callbacks.html#otp-verification-callbacks).

## Automatic SMS capture

**SMS codes can be captured automatically.** The SDK computes the app's SMS Retriever hash,
sends it with each SMS verification, and starts listening only when the API response echoes the
same hash. When a matching message arrives, the flow moves to `Captured` and submits the code
without the user typing it. Handle `VerificationState.Captured` as a normal part of your
flow.

**The SMS Retriever app hash does not need manual configuration.** The SDK computes it at
runtime from the certificate used to sign the installed APK. This is the certificate that
Google Play services checks when matching incoming SMS messages.

No extra configuration is needed for Play App Signing. If Google re-signs the app before
installation, the SDK reads the certificate used on the installed APK and computes the matching
hash from it.

Automatic capture begins after the create or resume response is decoded. An SMS that arrives
before that point cannot be captured by the SDK, so manual entry must remain available.

Note

Always provide manual entry as a fallback. Manual entry works for SMS and phone call and
remains available while automatic capture is listening. Automatic SMS capture requires
Google Play services and is not available when the app depends on `verification-core`
alone, because that artifact does not include the SMS channel.

`AwaitingInput.sms.interceptionTimeoutSeconds` is the automatic capture window, not the
verification deadline. It tells the SDK how long to keep listening for a matching SMS. When
the window ends, the SDK stops listening, but the verification remains active and manual entry
still works. It equals the application's **Code timeout** (60-600 seconds, 300 by default).
`expiresAtEpochMillis` is the value that defines when the verification expires.

The SDK writes limited diagnostics to Android Logcat under the `DidwwVerification` tag,
including app-hash availability, the computed app hash, and SMS Retriever re-arming. These
diagnostics do not include OTP codes, phone numbers, or credentials.

## Error handling

A failed verification is reported through the state flow rather than thrown as an exception.
Each verification outcome is represented as a `VerificationState`.

Two invalid calls throw `IllegalArgumentException` synchronously, before a request is sent:

- Passing `sms` options to a non-SMS delivery method.
- Calling `resume(...)` with a destination that contains no digits.

`Failed` carries a `FailureReason` with either an API error or an SDK-side error:

```
when (val reason = state.reason) {
    is FailureReason.Api -> reason.error       // the server said no - an ApiErrorItem
    is FailureReason.Sdk -> when (reason.error) {
        is SdkError.Transport   -> "offline, timed out, or TLS failed"
        is SdkError.Decoding    -> "the response could not be read"
        SdkError.Superseded     -> "another verification replaced this one"
        SdkError.AlreadyRunning -> "states was collected twice - see Collect once"
    }
}
```

SDK-side failures have the following meanings:

| Error | Meaning |
| --- | --- |
| `SdkError.Transport` | The request did not complete because of DNS, connection, TLS, timeout, socket, or URL failure. A non-successful HTTP response without a usable API error envelope is also reported here. |
| `SdkError.Decoding` | A successful HTTP response could not be decoded as a verification. |
| `SdkError.Superseded` | A newer handle from the same SDK client replaced this handle for the same destination. |
| `SdkError.AlreadyRunning` | The handle's `states` flow was collected more than once. |

Server-side errors are returned as `ApiErrorItem` values, matching the API's
[coded error envelope](../api-reference/errors.html#otp-verification-api-errors):

| Property | Meaning |
| --- | --- |
| `code` | The raw slug, always present - for example `code_invalid`. |
| `detail` | Fixed human-readable text. Display it; never branch on it. |
| `known` | The typed `ApiErrorCode` when this SDK version recognises the slug, otherwise `null`. |
| `retryAfterSeconds` | The `Retry-After` header in whole seconds when the response carried one, otherwise `null`. |

```
when (error.known) {
    ApiErrorCode.CODE_INVALID      -> showError("That code is not right.")
    ApiErrorCode.TOO_MANY_ATTEMPTS -> showError("Too many attempts. Start over.")
    ApiErrorCode.BALANCE_INSUFFICIENT,
    ApiErrorCode.UNAUTHORIZED      -> reportToYourBackend(error.code)
    null                           -> showError(error.detail)   // a slug newer than this SDK
    else                           -> showError(error.detail)
}
```

`ApiErrorCode` does not have an `.other` case. If the SDK receives an error code it does
not recognise, `known` is `null` and `code` still contains the raw value. This lets the
SDK decode newer API error codes without losing the original code. Branch on `code` or
`known`, never on `detail`.

A start for the same number too soon after the previous one is refused with `429` and
surfaces as `Failed` with `ApiErrorCode.DESTINATION_IN_COOLDOWN`. Wait
`retryAfterSeconds` before starting again for that number; a start is never retried
automatically.

## Cancellation and timeouts

Cancellation uses standard coroutine cancellation. Because the request starts when `states`
is collected, cancelling the collecting coroutine cancels the in-flight request and releases
resources registered by the channel, including an active SMS listener. There is no separate
`stop()` method. When a `ViewModel` is cleared, its coroutine scope is cancelled
automatically. Cancellation ends collection without emitting a `Failed` state.

```
val job = viewModelScope.launch {
    handle.states.collect { _state.value = it }
}

job.cancel()   // cancels the request and releases everything the channel registered
```

Per-request timeouts are configured through `Config`. They are transport timeouts, not the
verification expiry policy. A verification expires according to the server-provided
`expiresAtEpochMillis` value:

```
DidwwVerification(
    context, auth, Environment.Production,
    Config(connectTimeoutMillis = 15_000, readTimeoutMillis = 30_000),   // the defaults
)
```

## One active verification per number

Only one unfinished verification can exist for the same application and phone number. If your
app collects a new start handle for a number that already has a verification in progress, the
new verification supersedes the previous one on the server.

The SDK marks the older handle as `SdkError.Superseded` as soon as another handle is created
by the same `DidwwVerification` instance with the same destination string. This local signal
can therefore arrive before the new handle is collected. Reuse one client instance across
starts so this in-process tracking remains available. Different formatting, such as
`+49 151...` and `49151...`, is not normalized for this local comparison.

If another client, process, device, or backend supersedes the verification, the current handle
learns about it only after its next request is rejected. The API does not push this update to
the SDK.

## Android SDK behavior

| Capability | On Android |
| --- | --- |
| Polling for status | The SDK does not provide `status()`. The `states` flow reports transitions caused by the handle's requests and local expiry countdown. |
| Resume by phone number | `resume(destination, method)` looks up the latest verification for the number and returns a new state-driven handle. Use it after process recreation when the original handle is no longer available. |
| Report requests | A handle created by `start(...)` reports by verification ID. A handle created by `resume(...)` reports through the by-number endpoint. The Android SDK uses `PUT` for both report forms. |
| Language | Kotlin. Coroutines and `Flow` are part of the public surface, so Java is not a supported integration path. |

## Sample application

The [Android SDK sample application](https://github.com/didww/didww-verification-android-sdk/tree/main/sample)
demonstrates
state rendering, lifecycle-aware collection, automatic SMS capture, repeated starts, and SDK
error handling.

On this page
