---
Source: https://docs.microblink.com/blinkcard/liveness
Title: Detect fraud and liveness
Description: Interpret BlinkCard liveness checks for screen recapture, photocopy, and hand presence, and tune their strictness
---

# Detect fraud and liveness

BlinkCard runs liveness checks while scanning to help you spot cards that aren't physically present.
Three checks run on each side of the card:

- **screen check**: detects a card displayed on a screen (a recapture)
- **photocopy check**: detects a photocopy of a card
- **hand presence check**: detects whether the card is being held in a human hand

:::note

The liveness checks, their result fields, and their settings are identical across the Web, Android, and iOS SDKs.
The code samples below show each platform.

:::

## Interpret the liveness result

The result exposes an overall verdict and the per-check, per-side detail.

`overallCardLivenessResult` passes only when every individual check passes, and fails if any check fails:

<Tabs queryString="platform">
  <TabItem value="web" label="Web (JS)" default>

Each check result is one of `"pass"`, `"fail"`, or `"not-available"`.

```ts
blinkCard.addOnResultCallback((result) => {
  if (result.overallCardLivenessResult === "fail") {
    // Treat the card as potentially fraudulent: reject or send to manual review
  }
});
```

  </TabItem>
  <TabItem value="android" label="Android (Kotlin)">

Each check result is one of `CheckResult.Pass`, `CheckResult.Fail`, or `CheckResult.NotAvailable`.

```kotlin
if (result.overallCardLivenessResult == CheckResult.Fail) {
    // Treat the card as potentially fraudulent: reject or send to manual review
}
```

  </TabItem>
  <TabItem value="ios" label="iOS (Swift)">

Each check result is one of `.pass`, `.fail`, or `.notAvailable`.

```swift
if result.overallCardLivenessResult == .fail {
    // Treat the card as potentially fraudulent: reject or send to manual review
}
```

  </TabItem>
</Tabs>

To see exactly which check failed, read the per-side `cardLivenessCheckResult`:

<Tabs queryString="platform">
  <TabItem value="web" label="Web (JS)" default>

```ts
const checks = result.firstSideResult?.cardLivenessCheckResult;

if (checks?.screenCheckResult === "fail") {
  // Card was likely displayed on a screen
}
if (checks?.photocopyCheckResult === "fail") {
  // Card was likely a photocopy
}
if (checks?.cardHeldInHandCheckResult === "fail") {
  // Card was not detected being held in a hand
}
```

  </TabItem>
  <TabItem value="android" label="Android (Kotlin)">

```kotlin
val checks = result.firstSideResult?.cardLivenessCheckResult

if (checks?.screenCheckResult == CheckResult.Fail) {
    // Card was likely displayed on a screen
}
if (checks?.photocopyCheckResult == CheckResult.Fail) {
    // Card was likely a photocopy
}
if (checks?.cardHeldInHandCheckResult == CheckResult.Fail) {
    // Card was not detected being held in a hand
}
```

  </TabItem>
  <TabItem value="ios" label="iOS (Swift)">

```swift
let checks = result.firstSideResult?.cardLivenessCheckResult

if checks?.screenCheckResult == .fail {
    // Card was likely displayed on a screen
}
if checks?.photocopyCheckResult == .fail {
    // Card was likely a photocopy
}
if checks?.cardHeldInHandCheckResult == .fail {
    // Card was not detected being held in a hand
}
```

  </TabItem>
</Tabs>

A "not available" result means the check did not run or could not produce a verdict for that side, for example when the check is disabled.
The same per-side detail is available on `secondSideResult?.cardLivenessCheckResult`.

## Tune detection strictness

Liveness sensitivity is configured through the `livenessSettings` inside the scanning settings.
Pass these settings when you start scanning:

<Tabs queryString="platform">
  <TabItem value="web" label="Web (JS)" default>

Pass `scanningSettings.livenessSettings` to `createBlinkCard`:

```ts
const blinkCard = await createBlinkCard({
  licenseKey: "your-license-key",
  scanningSettings: {
    livenessSettings: {
      screenCheckStrictnessLevel: "level-7",
      photocopyCheckStrictnessLevel: "level-7",
      enableCardHeldInHandCheck: true,
    },
  },
});
```

  </TabItem>
  <TabItem value="android" label="Android (Kotlin)">

Build a `LivenessSettings`, wrap it in `ScanningSettings` and `BlinkCardSessionSettings`, then pass that as the `sessionSettings` parameter of `BlinkCardCameraScanningScreen`:

```kotlin
val sessionSettings = BlinkCardSessionSettings(
    scanningSettings = ScanningSettings(
        livenessSettings = LivenessSettings(
            screenCheckStrictnessLevel = StrictnessLevel.Level7,
            photocopyCheckStrictnessLevel = StrictnessLevel.Level7,
            enableCardHeldInHandCheck = true,
        )
    )
)

BlinkCardCameraScanningScreen(
    blinkCardSdk = blinkCardSdk,
    sessionSettings = sessionSettings,
    onScanningSuccess = { result -> /* ... */ },
    onScanningCanceled = { /* ... */ },
)
```

  </TabItem>
  <TabItem value="ios" label="iOS (Swift)">

Build a `LivenessSettings`, wrap it in `ScanningSettings` and `BlinkCardSessionSettings`, then pass that as the `blinkCardSessionSettings` parameter of `BlinkCardAnalyzer`:

```swift
let sessionSettings = BlinkCardSessionSettings(
    scanningSettings: ScanningSettings(
        livenessSettings: LivenessSettings(
            enableCardHeldInHandCheck: true,
            screenCheckStrictnessLevel: .level7,
            photocopyCheckStrictnessLevel: .level7
        )
    )
)

let analyzer = try await BlinkCardAnalyzer(
    sdk: sdk,
    blinkCardSessionSettings: sessionSettings,
    eventStream: eventStream
)
```

  </TabItem>
</Tabs>

### Screen and photocopy strictness

`screenCheckStrictnessLevel` and `photocopyCheckStrictnessLevel` accept a "disabled" value or one of ten levels.
On web the values are `"disabled"` or `"level-1"` through `"level-10"`.
On Android and iOS they are the `StrictnessLevel` enum: `Disabled`/`.disabled` or `Level1`/`.level1` through `Level10`/`.level10`.
Both default to level 5.

- **type**: string
- Higher levels are stricter: they catch more fraud (lower false accepts) but may reject more genuine cards (higher false rejects).
- The disabled value turns the check off.

### Hand presence

Enable or disable the hand presence check with `enableCardHeldInHandCheck`.
When enabled, you can tune how a hand is recognized:

- `handToCardSizeRatio`: the minimum hand-to-card size ratio for a valid detection. Lower values accept smaller or more distant hands.
- `handCardOverlapThreshold`: the minimum overlap between the detected hand and card regions, used to ignore hands that are present but not holding the card.

:::warning[Test before changing defaults]

The default strictness levels are tuned to balance user experience against risk.
Adjust them based on your own risk tolerance, and test thoroughly before deploying changes.

:::

## What the BIN tells you

The leading digits of a payment card number form its **Bank Identification Number (BIN)**.
BlinkCard matches the scanned number against its BIN database and uses the match to fill in the card's issuer details.

Fields are populated from the BIN in two tiers:

- `issuingNetwork` (on the result) is filled whenever the BIN is matched.
- `fundingType`, `cardCategory`, `issuerName`, and `issuerCountryCode` (on each entry in `cardAccounts`) are filled only when the card number is also valid, that is when `cardNumberValid` is `true`.

See the [result reference](./result-reference.md#card-accounts) for the full list of fields and their types.

### Empty BIN fields as a fraud signal

When BlinkCard cannot match the scanned number against its BIN database, these fields come back empty (`undefined` on web, `null` on Android, `nil` on iOS).
An empty `issuingNetwork` means the scanned number could not be matched, which is a potential fraud signal worth routing to manual review.

The four issuer-detail fields are also empty when the card number fails validation, that is when `cardNumberValid` is `false`.

## Related articles

- [Scan a card via camera](./scan-camera.md)
- [Extract specific fields](./extract-fields.md)


Last updated on Jun 18, 2026
