> ## Documentation Index
> Fetch the complete documentation index at: https://developer.keverd.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Android SDK

> Device fingerprinting and login verify with com.keverd:fraud-sdk

# Android SDK

Same dashboard and API as the web SDKs. The Android SDK **collects** device signals with a **public** key and returns an `eventId`. Your backend **verifies** with a **secret** key.

## Requirements

* Min SDK **21**, Target SDK **36**
* Kotlin **1.9+** (Java 11 bytecode)
* Internet access on device

## Install

**Maven Central:**

```kotlin theme={null}
dependencies {
    implementation("com.keverd:fraud-sdk:1.0.0")
}
```

Private GitHub Packages publishing is also supported for internal builds — use the same `com.keverd:fraud-sdk` coordinates when configured.

## API keys

Use a **public** key only (`kv_pk_test_…` / `kv_pk_live_…`).

<Warning>
  Never put a secret key (`kv_sk_…`) in the Android app. Secret keys are for [server verify](/server/overview) only.
</Warning>

Get keys at [dashboard.keverd.com](https://dashboard.keverd.com) → **Integrate → API keys**. See [API keys](/api-keys).

## Permissions

Merged from the library manifest:

| Permission             | Required | Purpose                                          |
| ---------------------- | -------- | ------------------------------------------------ |
| `INTERNET`             | Yes      | API calls                                        |
| `ACCESS_NETWORK_STATE` | Yes      | Network signals                                  |
| `READ_PHONE_STATE`     | Optional | Richer SIM / mobile signals (request at runtime) |

## Initialize

Call once — typically in `Application.onCreate`:

```kotlin theme={null}
import com.keverd.sdk.Config
import com.keverd.sdk.Keverd

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        Keverd.init(
            this,
            Config(
                apiKey = BuildConfig.KEVERD_PUBLIC_KEY, // kv_pk_…
                endpoint = "https://api.keverd.com",   // optional
                consentRequired = true,                // default true
                debug = BuildConfig.DEBUG,             // optional
            ),
        )
    }
}
```

### `Config`

| Field             | Required | Default                  | Notes                                               |
| ----------------- | -------- | ------------------------ | --------------------------------------------------- |
| `apiKey`          | **Yes**  | —                        | Public key (`kv_pk_…`)                              |
| `endpoint`        | No       | `https://api.keverd.com` | API base URL                                        |
| `userId`          | No       | `null`                   | Optional integrator id                              |
| `consentRequired` | No       | `true`                   | When `true`, call `setConsent(true)` before collect |
| `debug`           | No       | `false`                  | Logs OkHttp request/response                        |

## Consent

When `consentRequired = true` (default):

```kotlin theme={null}
Keverd.setConsent(true)
```

Without consent, `getVisitorData()` fails with `KeverdError.Code.CONSENT_REQUIRED` (or returns `Result.ConsentRequired` on the callback API).

## Collect

### Kotlin (suspend)

```kotlin theme={null}
lifecycleScope.launch {
    try {
        val data = Keverd.getVisitorData()
        // Send data.eventId to your backend
        Log.d("Keverd", "visitor=${data.visitorId} risk=${data.riskScore} action=${data.action}")
    } catch (e: KeverdError) {
        Log.e("Keverd", "${e.code}: ${e.message}")
    }
}
```

### Callback (Java-friendly)

```kotlin theme={null}
Keverd.getVisitorData { result ->
    when (result) {
        is Result.Success -> {
            val data = result.data
            // data.eventId → your backend
        }
        is Result.ConsentRequired -> { /* show consent UI */ }
        is Result.Error -> {
            Log.e("Keverd", "${result.error.code}: ${result.error.message}")
        }
    }
}
```

`getVisitorData()` calls **`POST /v2/fingerprint`** with header `X-API-Key` (and `X-SDK-Source: android`).

## Response (`VisitorData`)

| Field                                            | Type           | Notes                                                |
| ------------------------------------------------ | -------------- | ---------------------------------------------------- |
| `visitorId`                                      | `String`       | Stable device id (`kvd_…`, from API `fingerprint`)   |
| `eventId`                                        | `String`       | **Send this to your server** for verify              |
| `requestId`                                      | `String`       | Same id family as `eventId`                          |
| `sessionId`                                      | `String`       | Session id                                           |
| `riskScore`                                      | `Int`          | 0–100 style score from collect                       |
| `score`                                          | `Double`       | Normalized score                                     |
| `action`                                         | `Action`       | `ALLOW`, `SOFT_CHALLENGE`, `HARD_CHALLENGE`, `BLOCK` |
| `reasons`                                        | `List<String>` | Reason codes                                         |
| `similarity`                                     | `Double?`      | Optional                                             |
| `isNew` / `timesSeen` / `firstSeen` / `lastSeen` | optional       | Device history hints                                 |
| `adaptiveResponse`                               | optional       | Challenge hints when present                         |

Send the event id to your backend:

```json theme={null}
{
  "eventId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```

Your server then calls [POST /v2/verify](/api-reference/verify) with `{ "event_id": "…" }` using a **secret** key. See [Server SDKs](/server/overview).

<Note>
  There is no separate `getVisitorId()` API. Use `VisitorData.visitorId` after `getVisitorData()`.
</Note>

## Login verification (collect with login context)

Attaches login context to the collect request (`POST /fingerprint/verify/login`). This still uses the **public** key and does **not** replace server `verify()`.

```kotlin theme={null}
import com.keverd.sdk.util.LoginIdentifierHash
import com.keverd.sdk.verify.AuthMethod
import com.keverd.sdk.verify.LoginContext
import com.keverd.sdk.verify.LoginResult
import com.keverd.sdk.verify.VerifyLoginOptions

val data = Keverd.verifyLogin(
    VerifyLoginOptions(
        login = LoginContext(
            identifierHash = LoginIdentifierHash.fromIdentifier(email),
            result = LoginResult.SUCCESS, // or FAILURE
            authMethod = AuthMethod.PASSWORD,
            mfaUsed = true,
        ),
    ),
)
// Still send data.eventId to your backend for authoritative verify
```

`LoginIdentifierHash.fromIdentifier(identifier)` produces `sha256:<hex>` of the lowercased identifier (optional salt). Never send raw emails/phones as the hash input if your policy forbids it — hash client-side first.

### `LoginContext`

| Field            | Type          | Notes                                            |
| ---------------- | ------------- | ------------------------------------------------ |
| `identifierHash` | `String`      | Prefer `LoginIdentifierHash.fromIdentifier(...)` |
| `result`         | `LoginResult` | `SUCCESS` \| `FAILURE`                           |
| `failureReason`  | `String?`     | Optional                                         |
| `authMethod`     | `AuthMethod`  | e.g. `PASSWORD`, `SSO`, `PASSKEY`, `UNKNOWN`     |
| `mfaUsed`        | `Boolean?`    | Optional                                         |
| `attemptId`      | `String?`     | Optional                                         |

## Behavioral signals (optional)

Improve signal quality by attaching listeners on screens where users interact:

```kotlin theme={null}
// Activity — touch kinematics
Keverd.attachBehavioralMonitoring(this)

// Login / form fields — keystroke timing
Keverd.attachToEditText(binding.emailEditText)
Keverd.attachToEditText(binding.passwordEditText)

// Or a custom touch listener
view.setOnTouchListener(Keverd.createTouchListener())
```

## Preview signals (debug)

```kotlin theme={null}
val snapshot = Keverd.previewCollectedSignals()
// Local signals only — does not call the API
```

Useful in the sample app / debug builds. Do not use as a substitute for `getVisitorData()`.

## Errors

Suspend APIs throw `KeverdError`. The callback API returns `Result.Error(KeverdError)`.

| `KeverdError.Code` | When                                |
| ------------------ | ----------------------------------- |
| `NOT_INITIALIZED`  | `Keverd.init` not called            |
| `CONSENT_REQUIRED` | Consent gate blocked collect        |
| `UNAUTHORIZED`     | HTTP 401 (bad / missing public key) |
| `INVALID_REQUEST`  | HTTP 400                            |
| `NETWORK_ERROR`    | Timeout / connectivity              |
| `SERVER_ERROR`     | HTTP 5xx                            |
| `PARSE_ERROR`      | Unexpected / incomplete API payload |
| `UNKNOWN`          | Other failures                      |

```kotlin theme={null}
try {
    Keverd.getVisitorData()
} catch (e: KeverdError) {
    when (e.code) {
        KeverdError.Code.CONSENT_REQUIRED -> { /* prompt user */ }
        KeverdError.Code.UNAUTHORIZED -> { /* check kv_pk_ key */ }
        KeverdError.Code.NETWORK_ERROR -> { /* retry */ }
        else -> Log.e("Keverd", e.message, e)
    }
}
```

## ProGuard / R8

The published AAR ships consumer ProGuard rules that keep the public SDK surface (`Keverd`, `Config`, `VisitorData`, `Result*`, `KeverdError`, `Action`, verify types, and API DTOs). Extra keep rules are usually unnecessary.

## Deprecated API

`KeverdFingerprint.init` / `submit(...)` remains as a thin 1.0.x migration wrapper. Prefer:

```kotlin theme={null}
Keverd.init(context, config)
Keverd.setConsent(true)
Keverd.getVisitorData()
```

## End-to-end flow

1. App calls `Keverd.getVisitorData()` with `kv_pk_…`
2. App sends `eventId` to your backend
3. Backend calls [POST /v2/verify](/api-reference/verify) with `kv_sk_…` and `{ "event_id": "…" }`
4. Enforce returned `action`: `allow` | `soft_challenge` | `hard_challenge` | `block`

Optional: subscribe to [webhooks](/server/webhooks) after verify.

## Next

* [Server SDKs](/server/overview)
* [Node.js verify](/node-js)
* [POST /v2/verify](/api-reference/verify)
* [API keys](/api-keys)
