# Introduction 👋🏻

![Card to JSON](/files/9Am7ff3WWHJMwPclprsa)

## Getting Started

[Cardscan.ai](https://www.cardscan.ai) is the **fastest way** to add insurance card scanning to your mobile (iOS, Android) application, web application, or backend systems.

{% hint style="success" %}
Our UI components for React and mobile can be integrated in **5 minutes or less.**
{% endhint %}

### Design Goals 🎨

Get an overview of our API design and the machine learning algorithms powering [CardScan.ai](https://www.cardscan.ai) on the [Design Goals](/design-goals) page.

### Authentication 🔐

**Data security is our #1 focus.** This API has been built with robust authentication and authorization to protect our users. Read more on the [Authentication](/authentication) page.

### Developer API 💻

Our API enables developers to build a custom integration or leverage [CardScan.ai](https://www.cardscan.ai) as part of backend workflows. See API details and code examples on the [Developer API](/api) page.

### UI Components 📲

Simple and configurable components can be dropped into web or mobile applications in minutes.

* [React (JS/TS)](/ui-components/react) - Camera-based scanning
* [React DropZone](/ui-components/react-dropzone) - Drag & drop file upload
* [React Native](/ui-components/react-native)
* [Flutter](/ui-components/flutter)
* [iOS (Swift)](/ui-components/ios)
* [Android (Kotlin)](/ui-components/android)

### API Clients 📦

Pre-built client libraries with type-safe interfaces for easy integration:

* [TypeScript/JavaScript](/api-clients/typescript)
* [Python](/api-clients/python)
* [Swift](/api-clients/swift)
* [Kotlin](/api-clients/kotlin)
* [Dart](/api-clients/dart)

### Advanced Features 🚀

Take advantage of powerful features to enhance your integration:

* [Camera Permission Modal](/advanced-features/camera-permission-modal)
* [Eligibility Verification](/advanced-features/eligibility-verification)
* [Overseer Rules Engine](/advanced-features/overseer)
* [Payer Matching](/advanced-features/payer-matching)
* [Web to Mobile Handoff](/advanced-features/web-to-mobile-handoff)
* [Webhooks](/advanced-features/webhooks)

### AI & Automation 🤖

Streamline healthcare workflows with AI-powered automation:

* [Healthcare MCP Server](/ai-and-automation/healthcare-mcp-server) - Connect AI agents (Claude, ChatGPT, etc.) to CardScan for automated eligibility verification and card processing

{% hint style="info" %}
Need a different UI Component or have questions about the API? Let us know: <team@cardscan.ai>
{% endhint %}


# Design Goals 🎨

We built [CardScan.ai](https://www.cardscan.ai) with three guiding principles:

1. **Security**
2. **Accuracy**
3. **Performance**

### Security

Our infrastructure, APIs, and UI Components have been built from the ground up with **security and privacy in mind**. Keeping our client's data and their user's data secure is our number one priority.

To do this we built a best-in-class security architecture, similar to what Google and Apple\* use to secure data for millions.

We use **defense in depth**, starting with a robust [authentication](/authentication#end-user) model which can be deployed on the end users' devices with minimal security exposure. Our authorization layer then limits access to the host, API, and resources. We are also tightly integrated with AWS's Identity and Access Management (IAM) framework allowing the use of [roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) and [permissions boundaries](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) adding further restrictions on a per-user basis.

### Accuracy

The long tail in insurance card scanning can cause problems, especially with DIY solutions. We've solved this problem by building a large database of labeled card images to use in training our models.

> There are over **2,200** health insurers in the US market and **14,000+** unique insurance card formats and layouts being used every day.

Bad lighting, damaged cards, new layout, ambiguous fields -- it is essential that [Cardscan.ai](https://www.cardscan.ai/) provides accurate scanning results in all challenging conditions.

We start with making sure the source image is as **clear, complete, and as useable** as possible.

![The real world is never this easy!](/files/-Mg8RWuM904TI9EOiZ-p)

After being uploaded to our servers card images are processed by multiple different OCR, NLP, and DNN models. At the end of the process, we classify all elements on the card, but only return the ones above our detection threshold. All results are returned with a list of **confidence scores** from each stage.

Read more about our [machine learning](#machine-learning) pipeline below.

### Performance

All of our production infrastructure runs on a [multi-region architecture](https://aws.amazon.com/solutions/implementations/multi-region-application-architecture/) with auto-scaling resources and provisioned concurrency. For our models, we deploy on auto-scaling instances with modern GPUs that provide capacity for 1000s of executions per second.

Our API allows the end user to securely upload direct to [S3 Edge](https://aws.amazon.com/s3/transfer-acceleration/) locations, providing high performance in low bandwidth settings.

Individual API endpoints respond in **milliseconds** and the end-to-end processing should take **2-5 seconds** depending on system load and card complexity.

We have built our UI Components to provide progress indications and visual feedback to the end user while the card is being processed.

### Machine Learning

Our machine learning pipeline is comprised of four parts each with custom models:

![](/files/-Mg93c4v0fpbfrFLfssP)

The output of this pipeline is the label of each element (e.g. `member_id` ) the corresponding value (e.g `128845682`) and the probability. We represent the probability as a list with entries for each section of the pipeline.

```json
"member_number": {
  "value": "128845682",
  "scores": ["0.9994", "0.9828"]
}
```

![](/files/-Mg99s1JmOrGPvrXKN8s)

{% hint style="info" %}
The **last two elements** on the probability list are only included when a low score is detected in the first two.
{% endhint %}

#### Card Detection

All of our [UI Components](/ui-components/ios) run a custom ML model to detect insurance cards and make sure they are in-frame and in focus. We then apply image pre-processing to reduce noise and correct bad lighting.

The detection model runs **on-device** and is trained on **over 8000 images** of cards in a variety of lighting conditions.

![](/files/-Mg9AlI0dh82U8EraO8c)

{% hint style="info" %}
If you would like to use the card detection model in your own application without using our UI Components, please let us know.
{% endhint %}

#### Text Extraction

Once we have a high-quality card image uploaded to AWS S3 the image is run through an optical character recognition (OCR) model. This model extracts text out of the image and returns it in a machine-readable format.

#### Information Extraction

After the text has been extracted, it is run through a natural language processing (NLP) model which uses named-entity recognition (NER) to extract and label the results. This model connects the value `128845682` with the label `member_number` and provides a probability for the match `0.9943`

This model is fine-tuned with **over 6000 labeled text extraction results.**

#### Error Correction

The error correction process involved **5 custom models** which are run on select elements or on an as-needed basis.

The highest importance elements on the card, `group_number` and `member_number` , are checked for accuracy with a custom LSTM model. This model is trained on **over 600,000** cards.

Cards from one payer that represent around 5% of our volume do not perform well in our standard OCR pipeline. We've built a **custom OCR-DNN** to address this weakness.

The remaining 3 models are run on a small percentage of problem cards and help to fix errors in the Information Extraction process.

\----

\* - Members of our team have worked on security at Apple.


# Authentication 🔐

This API supports authentication for two different kinds of use cases:

* [**Server-to-server**](#server-to-server) - backend systems, admin portals, etc.
* [**End User**](#end-user) - a patient or clinician most likely on mobile or the web.

Jump down to example [authentication patterns](#authentication-pattern)

### Server-to-server

[CardScan.ai](https://www.cardscan.ai) authenticates server-to-server (S2S) API requests using your account's API keys. A request without an API key, or with an expired, or revoked key, will cause the API to return an error.

Every account has separate keys for testing on our sandbox, or for running live in production. The sandbox API is identical to the production API.

{% hint style="warning" %}
The sandbox API is **not** HIPAA compliant and should **NOT** be used for PHI.
{% endhint %}

#### API Keys

Your API Keys are available on the Dashboard. The API Keys start with a prefix to clearly distinguish their usage.

For accessing the API in the **sandbox** environment use keys with this format:

* `sk_test_cardscan_ai_XXXXXXXXXXXXXXX`

When you are ready for **production** or **live** mode, use keys with this format:

* `sk_live_cardscan_ai_XXXXXXXXXXXXXXX`

{% hint style="info" %}
**Note:** Legacy API keys in the format `secret_test_` and `secret_live_` are still supported but new keys use the `sk_test_cardscan_ai_` and `sk_live_cardscan_ai_` format.
{% endhint %}

Read more about sandbox vs live mode on the [API Endpoints](/api#api-endpoints) page

{% hint style="warning" %}
API Keys are like passwords, keep them safe and **NEVER** use them in a client-side application.
{% endhint %}

### End User

End users on all platforms (web, mobile, etc) authenticate with the [Cardscan.ai](https://www.cardscan.ai) APIs using a `sessionToken`. This token is a short-lived JSON Web Token (JWT).

Requesting a token is done via the [Access Token](/api#get-access-token) endpoint.

{% tabs %}
{% tab title="Bash" %}

```bash
curl --request GET 'https://sandbox.cardscan.ai/v1/access-token' \
--header 'Authorization: Bearer sk_test_cardscan_ai_XXXXXXXXXXXXXXX'
```

{% endtab %}

{% tab title="Python" %}

```python
import requests

url = "https://sandbox.cardscan.ai/v1/access-token"

headers = {
  'Authorization': 'Bearer sk_test_cardscan_ai_XXXXXXXXXXXXXXX'
}

response = requests.request("GET", url, headers=headers)
print(response.json())
```

{% endtab %}

{% tab title="Node.js" %}

```javascript
var axios = require('axios');

url = 'https://sandbox.cardscan.ai/v1/access-token'

var options = {
    headers: { 
        'Authorization': 'Bearer sk_test_cardscan_ai_XXXXXXXXXXXXXXX'
    }
}

axios.get(url, options)
  .then(function (response) {
    console.log(JSON.stringify(response.data))
  })
  .catch(function (error) {
    console.log(error);
});
```

{% endtab %}
{% endtabs %}

By default end users lose access to uploaded cards and all associated data when their session token expires. To prevent this pass in a `user_id` as a query parameter to the `/access-token` endpoint. The `user_id` parameter **must be unique** across your user base, we recommend using an email address or internal `uuid` identifier.

{% hint style="warning" %}
**WARNING:** using a non-unique **user\_id** will result in PHI exposure
{% endhint %}

```bash
curl --request GET 'https://sandbox.cardscan.ai/v1/access-token?user_id=d77176fb-be40-4884-b9bb-ca64f657804b' \
--header 'Authorization: Bearer sk_test_cardscan_ai_XXXXXXXXXXXXXXX'
```

{% hint style="info" %}
**Server-to-server** requests continue to have access to uploaded cards and associated data, even after the end user's session has expired.
{% endhint %}

### Authentication Pattern

The recommended pattern for authenticating end users is to create a [CardScan.ai](https://www.cardscan.ai) authentication endpoint on the customer's backend servers. In the diagram below the endpoint is called `/cardscan-session` and is responsible for authenticating the end user before requesting a session token from the [CardScan.ai](https://www.cardscan.ai) API.

![Auth Diagram](/files/-MfsNfloF9y1fiSwRok7)

Below are two overly simplified examples of this workflow for **Flask** and **Express**:

{% tabs %}
{% tab title="Flask Example" %}

```python
import requests
from flask_login import login_required, current_user


@app.route('/cardscan-session')
@login_required
def session():
    '''
    Generates a cardscan.ai token for the logged-in user and returns it.
    '''
    url = "https://sandbox.cardscan.ai/v1/access-token"
    params = {
        'user_id': current_user.id
    }
    headers = {
        'Authorization': 'Bearer sk_test_cardscan_ai_XXXXXXXXXXXXXXX'
    }
    response = requests.request("GET", url, params=params, headers=headers)
    response.raise_for_status()
    payload = response.json()

    return jsonify(payload)

```

{% endtab %}

{% tab title="Node.js Express Example" %}

```javascript
const router = express.Router();
var axios = require('axios');

router.get('/cardscan-session', (req, res) => {

    url = 'https://sandbox.cardscan.ai/v1/access-token'

    var options = {
        headers: { 
            'Authorization': 'Bearer sk_test_cardscan_ai_XXXXXXXXXXXXXXX'
        },
        params: {
            'user_id': req.auth.user
        }
    }

    axios.get(url, options)
      .then(function (response) {
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify(response.data));
      })
      .catch(function (error) {
        //Handle error
        res.status(error.response.status);
        res.end(error.response.data.message);
    });
});

```

{% endtab %}
{% endtabs %}

Once a `session` has been generated, it can be used to initialize the SDK and UI Components, or used to call the API directly. This allows the end user's browser or mobile device to safely and securely connect with the [CardScan.ai](https://cardscan.ai/) servers.

{% tabs %}
{% tab title="Swift Client" %}

```swift
private func didTapScanCard(_ sender: UIButton) {

    button.showLoaderAboveImage(userInteraction:true)

    var request = URLRequest(url: URL(string: "https://{{YOUR_SERVER_BASE_URL}}/cardscan-session")!)
    request.httpMethod = "GET"
    request.setValue("application/json", forHTTPHeaderField: "Accept")
      
    URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
        
           ///Extract session from server response
            guard error == nil,
              let data = data,
              let json = try? JSONDecoder().decode([String: String].self, from: data),
            let session = json["session"] else {

            //handle errors calling server. :-(
            print("Error calling server: ", error as Any)
            return
        }
        
        ///Trigger presentation of CardScannerView with user's session token.
        DispatchQueue.main.async { [weak self] in
            self?.presentCardScanner(userToken: session)
        }
    }.resume()
}

private func presentCardScanner(userToken: String) {
    
    ///Create CardScannerView with user session token
    let cardScannerViewController = CardScannerViewController(userToken: userToken, live: true)
    
    cardScannerViewController.present(from: self, animated:true) { result in 
        print(result)
    }
}
```

{% endtab %}

{% tab title="React Client" %}

```jsx
import { useEffect, useState } from "react";
import { CardScanView } from "@cardscan.ai/insurance-cardscan-react";

const Onboarding = () => {
  const [token, setToken] = useState("");
  
  const onSuccess = (card: any) => {
    console.log("success!");
  };
  
  const loadScanView = () => {  
  
    fetch("https://{{YOUR_SERVER_BASE_URL}}/cardscan-session", {
      method: "POST",
    })
      .then((res) => res.json())
      .then((data) => {
        setToken(data.Token);
      })
      .catch((err) => console.log(err));
  };
    
    return (
      <div>
        { (token.trim() == "") ?
        <button onClick={loadScanView}>Start Scanning</button>;
        : 
        <CardScanView
          live={false}
          sessionToken={token}
          onSuccess={onSuccess}
        />
      });
};

export default Onboarding;
```

{% endtab %}

{% tab title="Kotlin Client" %}

```kotlin
class OnboardingActivity : AppCompatActivity(), CardScanActivityResult {

    lateinit var cardScanResultLauncher : ActivityResultLauncher<Intent>

    private fun getSession(callback: (session: String) -> Unit) {
        val httpAsync = "https://{{YOUR_SERVER_BASE_URL}}/cardscan-session"
            .httpPost()
            .responseJson { _, _, result ->
                //check for errors :)
                val jsonObject = result.get().obj()
                val session = jsonObject["session"] as String
                callback(session)
            }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_onboarding)

        
        cardScanResultLauncher =  registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
           CardScanActivity.processResult(this@OnboardingActivity, result)
       }

        findViewById<View>(R.id.scanCardButton).setOnClickListener { _ ->
            getSession { session ->
                //trigger the loading of CardScanActivity with user's session token
                CardScanActivity.start(
                    activity = this,
                    resultLauncher = cardScanResultLauncher,
                    sessionToken = session
                )
            }

        }
    }

    override fun scanSuccess(card: CardData) {
        Log.d("CardScan", "ScanSuccess $card")
    }

}
```

{% endtab %}
{% endtabs %}


# API 💻

## Postman Collection

[![](https://run.pstmn.io/button.svg)](https://www.postman.com/cardscan/workspace/cardscan-workspace/collection/17501274-09778b26-c4e7-47af-bec2-a338f8b38f57?action=share\&creator=17501274)

## OpenAPI Specification

The complete OpenAPI 3.0 specification for the CardScan API is available at:

<https://github.com/CardScan-ai/api-clients/blob/main/openapi.yaml>

Use this specification to:

* Generate API clients in any language
* Import into API testing tools
* View detailed schema definitions
* Integrate with development tools

## Testing vs Production

After creating an account you can access the test API endpoints on our sandbox server:

{% embed url="<https://sandbox.cardscan.ai>" %}

This endpoint is identical to our production endpoint but **does not allow PHI** and only returns dummy card results.

Once you have signed our **Business Associate Agreement (BAA)** you will be able to access the production server:

{% embed url="<https://api.cardscan.ai>" %}

## Admin Endpoints

The admin endpoints are designed for managing access tokens within our system. These endpoints can only be called using a valid API key. They provide a secure way to generate and manage tokens, ensuring that only authorized users can access sensitive data and functionalities.

### Get Access Token

<mark style="color:blue;">`GET`</mark> `https://sandbox.cardscan.ai/v1/access-token`

This endpoint generates the JSON Web Token (JWT) for a web or mobile user

#### Query Parameters

| Name     | Type   | Description                    |
| -------- | ------ | ------------------------------ |
| user\_id | String | Unique identifier for the user |

{% tabs %}
{% tab title="200" %}

```json
{
  "IdentityId": "us-east-1:0236cb77-ac53-460d-ba39-60b186af2897",
  "Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}
```

{% endtab %}

{% tab title="401" %}

```json
{
  "message": "Unknown authorization error - please check your token format and try again", 
  "type": "Unauthorized",
  "code":401 
}
```

{% endtab %}

{% tab title="403" %}

```json
{
  "message": "API Key is not registered",
  "type": "Unauthorized",
  "code": 401
}
```

{% endtab %}
{% endtabs %}

This endpoint generates a short-lived session token that the web/mobile user will use to directly authenticate with CardScan's servers. See the [Authentication](/authentication#end-user) page for more details.

{% hint style="danger" %}
This function takes an optional `user_id` which **must be unique** between your users.
{% endhint %}

See [Authentication](/authentication#end-user) for code examples.

***

## Insurance Card Scanning Endpoints

### Create Card

<mark style="color:green;">`POST`</mark> `https://sandbox.cardscan.ai/v1/cards`

Creates a card object to initiate the scanning process and will be used for generating upload endpoints.

#### Request Body

| Name                   | Type    | Description                                                                                                                                          |
| ---------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| enable\_backside\_scan | boolean | <p>Enabled scanning of both sides of the insurance cards. Important for eligibility checks and prior auth.</p><p><strong>Default</strong>: False</p> |
| enable\_livescan       | boolean | <p>Allows the card to process multiple updates per side. Required for live scanning.</p><p><strong>Default</strong>: False</p>                       |

{% tabs %}
{% tab title="201: Created Successful Operation" %}

```json
{
    "card_id": "171d11c4-9154-4f2f-b4ca-0fb610abe05e",
    "state": "pending",
    "created_at": "2024-02-05 15:50:14.856792"
}
```

{% endtab %}

{% tab title="400: Bad Request Bad Request" %}

```json
{
    "message": "enable_backside_scan must be a boolean",
    "type": "Bad Request",
    "code": 400
}
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Curl" %}

```bash
curl --location 'https://sandbox.cardscan.ai/v1/cards' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer endusertokenXXX' \
--data '{
    "enable_backside_scan": true
}'
```

{% endtab %}
{% endtabs %}

### Generate Upload Url

<mark style="color:green;">`POST`</mark> `https://sandbox.cardscan.ai/v1/cards/:card_id/generate-upload-url`

Generates a URL and signed payload to enable direct image upload to AWS S3.

#### Path Parameters

| Name                                       | Type | Description                                  |
| ------------------------------------------ | ---- | -------------------------------------------- |
| card\_id<mark style="color:red;">\*</mark> | UUID | For the card entity to link the upload with. |

#### Request Body

| Name                                          | Type | Description                                                                                                                                                                                                                                   |
| --------------------------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| orientation<mark style="color:red;">\*</mark> | Enum | <p>'<strong>front</strong>' - the front face of the insurance card, often containing the name, member\_id, etc.</p><p>'<strong>back</strong>' - the back side of the card, often containing the claims address, and various phone number.</p> |

{% tabs %}
{% tab title="200 Successful Operation" %}

```javascript
{
  "card_id": "4c901279-622f-485f-85cc-aad1f12c2d8e",
  "upload_url": "https://cardscan-sandbox.s3.amazonaws.com/",
  "upload_parameters": {
    "key": "4c901279-622f-485f-85cc-aad1f12c2d8e",
    "x-amz-algorithm": "AWS4-HMAC-SHA256",
    "x-amz-security-token": "BADCAFEb3JpZ2sddsfdsfrIAiACLcnulBVymY22qkf3M8XDvlhXweRzDe8V95X73BextSqDAgiU//////////8BEAAaDDUzNTc5NTY3OTQ2NiIM1ZoFizA0PWXBXfiWKtcBVOcw4ss3VbqdPFO7OTJmxuPLUnPKNbsMaRF5sNEsqecP74+mucNgjAwigvQM3K0Bb/WqTsmGIkGXT7p+St1XpyB9nY+nVE1cVgtkggwU4UDXhBpnZLhQIE5fLjEBU+X0Gwd7WVXJdwXFa5iD+EwsQU2aJihBc1tdxYXmIZIkMQ1UXdFPbRc1vX4wcDgzrfbvT5749d1rOh5u9mKxTNc7z/1G4aYswrODlh5N/hSamcKGlaC/wwxrv8gwY64QEp/nQkUuTdrKPSrQRUb7Z5Fj7m8erRixTtYI2rOfAl5Cl",
    "x-amz-credential": "FAKEKEYSDONOTUSEXZYABC/20210413/us-east-1/s3/aws4_request",
    "x-amz-date": "20210413T210019Z",
    "policy": "eyJleHBpcmF0aW9uIjogIjIwMjEtMDQtMTNUMjI6MDA6MTlaIiwgImNvbmRpdGlvbnMiOiBbeyJidWNrZXQiOiAiY2FyZHNjYW4tc2FuZGJveCJ9LCB7ImtleSI6ICI0YzkwMTI3OS02MjJmLTQ4NWYtODVjYy1hYWQxZjEyYzJkOGUifSwgeyJ4LWFtei1hbGdvcml0aG0iOiAiQVdTNC1ITUFDLVNIQTI1NiJ9LCB7IngtYW16LWNyZWRlbnRpYWwiOiAiQUtJSEFMUk9CSVZGTkZYWllBQkMvMjAyMTA0MTMvdXMtZWFzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LCB7IngtYW16LWRhdGUiOiAiMjAyMTA0MTNUMjEwMDE5WiJ9XX0=",
    "x-amz-signature": "40da5a6814f646d9ded38539e37d6badcafe808494b04c76d37ccc89a3e5d34d"
  }
}
```

{% endtab %}

{% tab title="400 Invalid parameters" %}

```javascript
{
    "message": "expiration must be between 100 and 3600",
    "type": "Bad Request",
    "code": 400
}
```

{% endtab %}

{% tab title="401 Missing token or other error" %}

```javascript
{
    "message": "Unknown authorization error - please check your token format and try again",
    "type": "Unauthorized",
    "code": 401
}
```

{% endtab %}

{% tab title="403 Bad or expired token" %}

```javascript
{
    "message": "Token is expired",
    "type": "Authentication Timeout",
    "code": 419
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The S3 URL only supports HTTP **POSTs**, not PUTs.
{% endhint %}

The `upload_parameters` may change at any time, please make sure to not hardcode the list in any POST command.

{% tabs %}
{% tab title="Curl" %}

```bash
curl --location 'https://sandbox.cardscan.ai/v1/cards/9d709b16-3807-4fc9-b176-4409241beab5/generate-upload-url' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer endusertokenXXX \
--data '{   
    "orientation": "back"
}'
```

{% endtab %}

{% tab title="Javascript" %}

```javascript
// The JS client wraps generate-upload-url in with the file upload functionality.

const client = new CardScanApi({
    sessionToken:token, 
    live: false
});

client.uploadCardImage(file)
.then((cardId) => {
    //update UI.
})
.catch((error) => {
    //retry or update UI
});
```

{% endtab %}

{% tab title="Swift" %}

```swift
// The swift client wraps generate-upload-url with the file upload functionality.

let apiClient = CardScanAPIClient(userToken: userToken, live: false)

apiClient.uploadCardImage(image: image) { result in
    switch result {
    case .failure(let error):
        //check error, retry, update UI.
        print("uploadCardImage error \(error)")
    case .success(let cardId):
        //update UI for success!
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Image uploads have a **maximum** size of 50MB and a **minimum** of 5KB.
{% endhint %}

### List Scanned Cards

<mark style="color:blue;">`GET`</mark> `https://sandbox.cardscan.ai/v1/cards`

#### Query Parameters

| Name   | Type    | Description                                                                                         |
| ------ | ------- | --------------------------------------------------------------------------------------------------- |
| cursor | string  | Used to paginate through results when count is greater that limit.                                  |
| limit  | integer | <p>Count of cards to return.</p><p><strong>Default</strong>: 50</p><p><strong>Max:</strong> 200</p> |

{% tabs %}
{% tab title="200 Successful Response" %}

```javascript
{
    "cards": [
        {
            "card_id": "1bab2f3c-cfbe-4f22-b480-b389ece57f7e",
            "state": "pending",
            "created_at": "2021-07-30 19:56:40.638894+00:00",
        },
        {
            "card_id": "98405a1e-b367-4b3a-876a-e7886b7e1b69",
            "state": "processing",
            "created_at": "2021-07-30 20:26:40.838094+00:00",
        },
        {
            "card_id": "4c901279-622f-485f-85cc-aad1f12c2d8e",
            "state": "error",
            "created_at": "2021-07-30 20:58:50.334343+00:00",
            "error_message": "Failure during OCR process - [E507]"
        },
        {
            "card_id": "a1d743ee-3bb9-468d-a2c5-4e33fa0e1c6e",
            "state": "completed",
            "created_at": "2021-07-31 18:36:33.567313+00:00",
            "details": {
                "group_number": {
                    "value": "98755",
                    "scores": [
                        "0.9994",
                        "0.9828"
                    ]
                },
                "member_number": {
                    "value": "128845682",
                    "scores": [
                        "0.9984",
                        "0.9618"
                    ]
                },
                "member_name": {
                    "value": "emily dickinson",
                    "scores": [
                        "0.9974",
                        "0.8879"
                    ]
                },
                "plan_name": {
                    "value": "unitedhealthcare choice plus",
                    "scores": [
                        "0.9953",
                        "0.9835"
                    ]
                },
                "plan_id": {
                    "value": "(80840) 911-80708-01",
                    "scores": [
                        "0.9942",
                        "0.9939"
                    ]
                }
            }
        }
    ],
    "response_metadata": {
        "limit": 4,
        "next_cursor": "IieuzkckWq0"
    }
}
```

{% endtab %}

{% tab title="400 Invalid parameters" %}

```javascript
{
    "message": "limit must be a positve integer and less than 500",
    "type": "Bad Request",
    "code": 400
}
```

{% endtab %}

{% tab title="401 Missing or invalid token" %}

```javascript
{
    "message": "Unknown authorization error - please check your token format and try again",
    "type": "Unauthorized",
    "code": 401
}
```

{% endtab %}

{% tab title="403 Token is expired " %}

```javascript
{
    "message": "Token is expired",
    "type": "Authentication Timeout",
    "code": 419
}
```

{% endtab %}
{% endtabs %}

The `next_cursor` field is only present in the `response_metadata` when there are additional results to request.

The `completed` card in this example is truncated, for a full card and supported states, please see [Get Card](#get-card) below.

{% tabs %}
{% tab title="Curl" %}

```bash
curl --location --request GET 'https://sandbox.cardscan.ai/v1/cards' \
--header 'Authorization: Bearer endusertokenXXX' 
```

{% endtab %}

{% tab title="Javascript" %}

```javascript
const client = new CardScanApi({
    sessionToken:token, 
    live: false
});

client.listCards()
.then((cards) => {
    //update UI.
})
.catch((error) => {
    //retry or update UI
});
```

{% endtab %}

{% tab title="Swift" %}

```swift
let apiClient = CardScanAPIClient(userToken: userToken, live: false)

apiClient.listCards(limit: 10) { result in
    switch result {
    case .failure(let error):
        print("listCards error \(error)")
    case .success(let cards):
        //update UI
    }
}
```

{% endtab %}
{% endtabs %}

### Search Cards

<mark style="color:blue;">`GET`</mark> `https://sandbox.cardscan.ai/v1/cards/search`

Search through your card records using text queries to find specific cards by member name, payer, plan details, or other extracted information.

#### Query Parameters

| Name   | Type    | Description                                                                                               |
| ------ | ------- | --------------------------------------------------------------------------------------------------------- |
| query  | string  | **Required.** Search query to match against card data (member names, payer names, plan information, etc.) |
| cursor | string  | Used to paginate through results when count is greater than limit.                                        |
| limit  | integer | <p>Count of cards to return.</p><p><strong>Default</strong>: 50</p><p><strong>Max:</strong> 500</p>       |

{% tabs %}
{% tab title="200 Successful Response" %}

```javascript
{
    "cards": [
        {
            "card_id": "1bab2f3c-cfbe-4f22-b480-b389ece57f7e",
            "state": "completed",
            "created_at": "2021-07-30 19:56:40.638894+00:00",
            "details": {
                "member_name": {
                    "value": "john doe",
                    "scores": ["0.9974", "0.8879"]
                },
                "payer_name": {
                    "value": "unitedhealthcare",
                    "scores": ["0.9953", "0.9835"]
                }
            }
        }
    ],
    "response_metadata": {
        "limit": 50,
        "next_cursor": "IieuzkckWq0"
    }
}
```

{% endtab %}

{% tab title="400 Invalid parameters" %}

```javascript
{
    "message": "query parameter is required",
    "type": "Bad Request",
    "code": 400
}
```

{% endtab %}

{% tab title="401 Missing or invalid token" %}

```javascript
{
    "message": "Unknown authorization error - please check your token format and try again",
    "type": "Unauthorized",
    "code": 401
}
```

{% endtab %}

{% tab title="403 Token is expired" %}

```javascript
{
    "message": "Token is expired",
    "type": "Authentication Timeout",
    "code": 419
}
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Curl" %}

```bash
curl --location --request GET 'https://sandbox.cardscan.ai/v1/cards/search?query=john%20doe&limit=10' \
--header 'Authorization: Bearer endusertokenXXX' 
```

{% endtab %}

{% tab title="Javascript" %}

```javascript
const client = new CardScanApi({
    sessionToken:token, 
    live: false
});

client.searchCards("john doe", {limit: 10})
.then((cards) => {
    //update UI.
})
.catch((error) => {
    //retry or update UI
});
```

{% endtab %}

{% tab title="Swift" %}

```swift
let apiClient = CardScanAPIClient(userToken: userToken, live: false)

apiClient.searchCards(query: "john doe", limit: 10) { result in
    switch result {
    case .failure(let error):
        print("searchCards error \(error)")
    case .success(let cards):
        //update UI
    }
}
```

{% endtab %}
{% endtabs %}

### Get Card

<mark style="color:blue;">`GET`</mark> `https://sandbox.cardscan.ai/v1/cards/:cardId`

#### Path Parameters

| Name                                     | Type   | Description  |
| ---------------------------------------- | ------ | ------------ |
| cardId<mark style="color:red;">\*</mark> | string | UUID of card |

{% tabs %}
{% tab title="200 Successful response" %}

```javascript
{
    "card_id": "c1b93738-ddc0-4beb-9936-1f93fe0e4279",
    "state": "completed",
    "created_at": "2025-04-24 13:58:30.820353+00:00",
    "details": {
        "group_number": {
            "value": "98755",
            "scores": ["0.995", "0.999"]
        },
        "member_number": {
            "value": "128845682",
            "scores": ["0.995", "0.999"]
        },
        "member_name": {
            "value": "emily dickinson",
            "scores": ["0.994", "0.998"]
        },
        "dependent_names": [
            {
                "value": "richard dickinson",
                "scores": ["0.995", "0.999"]
            }
        ],
        "payer_name": {
            "value": "unitedhealthcare",
            "scores": ["0.737", "0.999"]
        },
        "payer_id": {
            "value": "87726",
            "scores": ["0.995", "0.999"]
        },
        "plan_name": {
            "value": "unitedhealthcare choice plus",
            "scores": ["0.967", "0.992"]
        },
        "rx_bin": {
            "value": "610279",
            "scores": ["0.995", "0.999"]
        },
        "rx_pcn": {
            "value": "9987",
            "scores": ["0.991", "0.999"]
        },
        "rx_issuer": {
            "value": "(80840) 911-80708-01",
            "scores": ["0.994", "0.999"]
        },
        "pharmacy_benefit_manager": {
            "value": "optumrx",
            "scores": ["0.601", "0.999"]
        }
    },
    "payer_match": {
        "cardscan_payer_id": "pay_8otorlr4",
        "cardscan_payer_name": "UNITEDHEALTHCARE",
        "score": "0.952",
        "matches": [
            {
                "clearinghouse": "Availity",
                "payer_id": "87726",
                "transaction_type": "professional",
                "payer_name": "UNITEDHEALTHCARE",
                "cardscan_payer_id": "pay_8otorlr4",
                "score": "0.952",
                "metadata": {
                    "last_updated": "2025-04-07T01:39:42.292212+00:00",
                    "source": "2025-04-06v1.0"
                }
            },
            {
                "clearinghouse": "Availity", 
                "payer_id": "87726",
                "transaction_type": "professional",
                "payer_name": "UNITEDHEALTHCARE DEFINITY HEALTH PLAN",
                "cardscan_payer_id": "pay_6lsgc6ns",
                "score": "0.9",
                "metadata": {
                    "last_updated": "2025-04-07T01:39:42.292312+00:00",
                    "source": "2025-04-06v1.0"
                }
            }
        ],
        "change_healthcare": [],
        "custom": [
            {
                "custom_payer_id": "UHC",
                "custom_payer_name": "United Healthcare",
                "custom_payer_name_alt": "United Healthcare Legacy",
                "score": "1.0",
                "source": "custom_payer_list_20240212"
            },
            {
                "custom_payer_id": "UHC",
                "custom_payer_name": "United Healthcare Oxford",
                "custom_payer_name_alt": "United Healthcare Legacy", 
                "score": "1.0",
                "source": "custom_payer_list_20240212"
            }
        ]
    },
    "metadata": {
        "insurance_scan_version": "malbec-1.0",
        "payer_match_version": "hybrid-1.2"
    },
    "images": {
        "front": {
            "url": "https://cardscan-sandbox-uploads-us-east-1.s3-accelerate.amazonaws.com/..."
        }
    },
    "deleted": false
}
```

{% endtab %}

{% tab title="400 Invalid UUID" %}

```javascript
{
    "message": "Invalid card_id, the ID must be formatted as a UUID.",
    "type": "Bad Request",
    "code": 400
}
```

{% endtab %}

{% tab title="401 Missing or invalid token" %}

```javascript
{
    "message": "Unknown authorization error - please check your token format and try again",
    "type": "Unauthorized",
    "code": 401
}
```

{% endtab %}

{% tab title="403 Token is expired" %}

```javascript
{
    "message": "Token is expired",
    "type": "Authentication Timeout",
    "code": 419
}
```

{% endtab %}

{% tab title="404 Card not found" %}

```javascript
{
    "message": "No resource found for card_id - a1d743ee-3bb9-468d-a2c5-4e33fa0e1c6a",
    "type": "Not Found",
    "code": 404
}
```

{% endtab %}
{% endtabs %}

Cards returned by this endpoint and the `/v1/cards` endpoint have a state field:

| Value          | Description                                                                       |
| -------------- | --------------------------------------------------------------------------------- |
| **pending**    | This card ID has been reserved but a corresponding file has not yet been uploaded |
| **processing** | The card is being processed by our ML pipeline.                                   |
| **completed**  | Card processing is completed and the card `details` are available.                |
| **error**      | An error has occurred, see `error_message` for more details.                      |
| **unknown**    | An unknown issue has occurred, please contact support for help.                   |

![](/files/-MgCf64OkCcHuY6VvkIn)

After being processed by the ML pipeline, each element is labeled and when the probability is above our minimum threshold the results are returned.

| Name                                                                                                                       | Description                                                                                                                      | Example                                                                            |
| -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| **group\_number**                                                                                                          | The group number identifies the specific benefits associated with the plan. This is missing with some payers and exchange plans. | *98755*                                                                            |
| **member\_number**                                                                                                         | A unique identifier for each member and dependent. This is used to verify coverage and arrange payment for services.             | *128845682*                                                                        |
| **payer\_name**                                                                                                            | The name of the health insurance payer or 3rd party administrator.                                                               | *unitedhealthcare*                                                                 |
| **payer\_id**                                                                                                              | The payer ID or EDI for the insurance company. This is as written on the card and not matched to a clearing house.               | *87726*                                                                            |
| <p><strong>rx\_bin</strong></p><p><strong>rx\_pcn</strong></p><p><strong>rx\_group</strong><br><strong>rx\_id</strong></p> | These are used to identify how a prescription drug will be reimbursed and where a pharmacy can send a reimbursement claim to     | <p><em>610279</em></p><p><em>9987</em></p><p><em>UHC</em><br><em>12458765</em></p> |
| **member\_name**                                                                                                           | Most often the policyholder, but on some cards this is the dependent the card was issues to.                                     | *Emily Dickinson*                                                                  |
| **dependent\_names**                                                                                                       | This is a list of all dependents found on the card.                                                                              | *Richard Dickinson*                                                                |
| **plan\_name**                                                                                                             | Our best attempt to determine the name of the plan for this card. This is missing on many cards.                                 | *unitedhealthcare choice plus*                                                     |
| **plan\_id**                                                                                                               | An identifier representing the plan associated with this card.                                                                   | *(80840) 911-80708-01*                                                             |
| **rx\_issuer**                                                                                                             | An identifier representing the prescription plan associated with this card.                                                      | *(80840) 911-80708-01*                                                             |
| **pharmacy\_benefit\_manager**                                                                                             | The pharmacy benefit manager (PBM) that processes prescription claims for this plan.                                             | *optumrx*                                                                          |
| **card\_specific\_id**                                                                                                     | A non-specific but prominent identifier found on the card.                                                                       | *54243*                                                                            |
| **client\_name**                                                                                                           | The name of the employer who is contracted with the 3rd party administrator.                                                     | *Apple, Inc.*                                                                      |
| **plan\_details**                                                                                                          | When available a list of: deductibles, co-pays, co-insurance, PCP name.                                                          | <p><em>Office: $25</em></p><p><em>ER: $300</em></p>                                |
| **start\_date**                                                                                                            | The date when coverage starts.                                                                                                   | *04/01/2021*                                                                       |
| **phone\_numbers**                                                                                                         | A list of all phone numbers found of the front and back of the card.                                                             | 800-400-5251                                                                       |
| **addresses**                                                                                                              | A list of all addresses found on the card.                                                                                       | <p>UnitedHealthcare</p><p>P.O. Box 740800</p><p>Atlanta, GA 30374-0800</p>         |

### Response Fields Outside of Details

| Name             | Description                                                                                                  | Example                                                                             |
| ---------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| **payer\_match** | Comprehensive payer matching results including clearinghouse matches, custom matches, and confidence scores. | *See* [*Payer Matching*](/advanced-features/payer-matching) *for details*           |
| **metadata**     | API version information including the insurance scan model version and payer match version used.             | *{"insurance\_scan\_version": "malbec-1.0", "payer\_match\_version": "hybrid-1.2"}* |
| **images**       | Signed URLs for accessing the uploaded card images. URLs are valid for 24 hours.                             | *{"front": {"url": "https\://..."}}*                                                |
| **deleted**      | Boolean indicating whether this card has been marked as deleted.                                             | *false*                                                                             |

{% hint style="info" %}
**Note:** If an element is missing from the `details` object, it means it is either not available on this type of card, or we did not have a high enough confidence to return it.
{% endhint %}

{% tabs %}
{% tab title="Curl" %}

```bash
curl --request GET 'https://sandbox.cardscan.ai/v1/cards/a1d743ee-3bb9-468d-a2c5-4e33fa0e1c6e' \
--header 'Authorization: Bearer endusertokenXXX'
```

{% endtab %}

{% tab title="Javascript" %}

```javascript
const client = new CardScanApi({
    sessionToken:token, 
    live: false
});

client.getCard(cardId)
.then((card) => {
    //update UI.
})
.catch((error) => {
    //retry or update UI
});
```

{% endtab %}

{% tab title="Swift" %}

```swift
let apiClient = CardScanAPIClient(userToken: userToken, live: false)

apiClient.getCard(cardUUID: cardId) { result in
    switch result {
    case .failure(let error):
        print("getCard error \(error)")
    case .success(let card):
        //update UI
    }
}
```

{% endtab %}
{% endtabs %}

### Delete Card

<mark style="color:red;">`DELETE`</mark> `https://sandbox.cardscan.ai/v1/cards/:cardId`

This will trigger a soft delete of the specified card.

#### Path Parameters

| Name                                     | Type   | Description      |
| ---------------------------------------- | ------ | ---------------- |
| cardId<mark style="color:red;">\*</mark> | String | UUID of the card |

{% tabs %}
{% tab title="204" %}

{% endtab %}

{% tab title="404" %}

{% endtab %}

{% tab title="403" %}

{% endtab %}
{% endtabs %}

{% hint style="info" %}
**Note**: All of the images, ML results, eligibility results, and PHI will be removed, but the card record and performance statistics will remain.
{% endhint %}

{% tabs %}
{% tab title="Curl" %}

```bash
curl --request DELETE 'https://sandbox.cardscan.ai/v1/cards/a1d743ee-3bb9-468d-a2c5-4e33fa0e1c6e' \
--header 'Authorization: Bearer endusertokenXXX'
```

{% endtab %}
{% endtabs %}

### Flag Card <a href="#flag-card" id="flag-card"></a>

<mark style="color:green;">`POST`</mark> `https://sandbox.cardscan.ai/v1/cards/:cardId/flag`

This endpoint allows you to flag a specific card with a particular type of flag. Flags are used to mark cards for special attention or action.

**Path Parameters**

| Name                                     | Type   | Description      |
| ---------------------------------------- | ------ | ---------------- |
| cardId<mark style="color:red;">\*</mark> | String | UUID of the card |

#### Request Body

<table><thead><tr><th width="149">Name</th><th width="111">Type</th><th width="232">Allowed values</th><th>Description</th></tr></thead><tbody><tr><td>flag_type<mark style="color:red;">*</mark></td><td>String</td><td><ul><li>missing_elements</li><li>incorrect_elements</li><li>bad_payer_match</li><li>unreadable_text</li><li>eligibility_issue</li><li>other</li></ul></td><td>The flag type used to mark the card</td></tr><tr><td>message</td><td>String</td><td></td><td>An optional message to add additional information</td></tr></tbody></table>

{% tabs %}
{% tab title="200 Successful response" %}

```json
{
    "message": "Card flagged successfully"
}
```

{% endtab %}

{% tab title="400 Flag type required" %}

```json
{
    "message": "flag_type is required",
    "type": "Bad Request",
    "code": 400
}
```

{% endtab %}

{% tab title="400 Invalid flag type" %}

```json
{
    "message": "Invalid flag_type",
    "type": "Bad Request",
    "code": 400
}
```

{% endtab %}
{% endtabs %}

## Magic Links

### Generate magic link

<mark style="color:green;">`GET`</mark> `/generate-magic-link`

Generates a magic link for the currently authenticated user. The generated token expires in one hour. This endpoint is called by the React Widget when using the [Web To Mobile Handoff](/advanced-features/web-to-mobile-handoff) feature.

{% tabs %}
{% tab title="200" %}

```json
{
    "magic_link": "https://sandbox.cardscan.ai/v1/validate-magic-link?token=<token>",
    "token": "<token>",
    "expires_at": "2024-06-27T03:48:01.049866"
}
```

{% endtab %}

{% tab title="403" %}

```json
{
    "message": "User is not authorized for this resource",
    "type": "Access Denied",
    "code": 403
}
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Curl" %}

```bash
curl --request GET 'https://sandbox.cardscan.ai/v1/generate-magic-link \
--header 'Authorization: Bearer endusertokenXXX'
```

{% endtab %}

{% tab title="Javascript" %}

```javascript
const client = new CardScanApi({
    sessionToken:token, 
    live: false
});

client.generateMagicLink()
.then((response) => {
    // ...
})
.catch((error) => {
    // ...
});
```

{% endtab %}
{% endtabs %}

### Validate magic link

<mark style="color:green;">`GET`</mark> `/validate-magic-link?token=<magic-token>`

Validates that the provided magic link token is valid and if so, returns an [Access Token](#get-access-token) with limited capabilities. You need to use this endpoint if you want to self-host the [Web To Mobile Handoff](/advanced-features/web-to-mobile-handoff) feature.

{% tabs %}
{% tab title="200" %}

```json
{
  "IdentityId": "us-east-1:0236cb77-ac53-460d-ba39-60b186af2897",
  "Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}
```

{% endtab %}

{% tab title="400: Invalid token" %}

```json
{
"message": "Invalid token",
"type": "Bad Request",
"code": 400
}
```

{% endtab %}

{% tab title="410: Expired token" %}

```json
{
"message": "Token expired - <token>",
"type": "Expired",
"code": 410
}
```

{% endtab %}

{% tab title="404" %}

```json
{
"message": "No resource found for token - badtokenbadtok",
"type": "Not Found",
"code": 404
}
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Curl" %}

```bash
curl --request GET 'https://sandbox.cardscan.ai/v1/validate-magic-link?token=<token> \
--header 'Authorization: Bearer endusertokenXXX'
```

{% endtab %}

{% tab title="Javascript" %}

```javascript
const token = "your-magic-token";

fetch(`${baseURL}/validate-magic-link?token=${token}`)
.then((res) => res.json())
.then((data) => {
  setToken(data.Token); // Access token
})
.catch((err) => console.log(err));

```

{% endtab %}
{% endtabs %}

## Eligibility Verification

Our Eligibility Verification Endpoints offer a seamless way to check the insurance eligibility of patients. They provide real-time verification of coverage details, ensuring accurate and efficient processing of healthcare services.

Please see the [#eligibility-verification](#eligibility-verification "mention") page for full details on the product offering.

Our Eligibility Verification Endpoints operate similarly to our Insurance Card Scanning Endpoints. Users can create a new eligibility request, which is processed asynchronously. The system updates the record as the state changes, providing real-time insights into insurance coverage and benefits.

{% hint style="info" %}
**Note:** Currently a completed insurance card scan is required to start the eligibility verification process.
{% endhint %}

{% openapi src="/files/G71NMY53wFIOacViGWdW" path="/eligibility" method="post" %}
[cardscan (1).yml](https://423317997-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MYkGp0C8rvjnJYLAI_u%2Fuploads%2F9obKv2uBd5nth7mRMHNH%2Fcardscan%20\(1\).yml?alt=media\&token=95d8f3f6-37cc-496e-a30c-c29b797af735)
{% endopenapi %}

To create an eligibility request, users must provide subscriber and dependent demographics along with provider details, including a National Provider Identifier (NPI). This information is essential for accurately verifying insurance eligibility.

You can locate a providers NPI on the [NPPES NPI Registry](https://npiregistry.cms.hhs.gov/search%23pageStart) search page.

{% openapi src="/files/G71NMY53wFIOacViGWdW" path="/eligibility/{eligibility\_id}" method="get" %}
[cardscan (1).yml](https://423317997-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MYkGp0C8rvjnJYLAI_u%2Fuploads%2F9obKv2uBd5nth7mRMHNH%2Fcardscan%20\(1\).yml?alt=media\&token=95d8f3f6-37cc-496e-a30c-c29b797af735)
{% endopenapi %}

{% openapi src="/files/G71NMY53wFIOacViGWdW" path="/eligibility" method="get" %}
[cardscan (1).yml](https://423317997-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MYkGp0C8rvjnJYLAI_u%2Fuploads%2F9obKv2uBd5nth7mRMHNH%2Fcardscan%20\(1\).yml?alt=media\&token=95d8f3f6-37cc-496e-a30c-c29b797af735)
{% endopenapi %}

## Health Check Endpoints

### Public Health Check

<mark style="color:blue;">`GET`</mark> `https://sandbox.cardscan.ai/v1/ping`

A public health check endpoint that returns the service status and feature flags. No authentication required.

{% tabs %}
{% tab title="200 Successful Response" %}

```json
{
    "timestamp": "2024-02-05T15:50:14.856792Z",
    "feature_flags": {
        "enable_advanced_ocr": true,
        "enable_payer_matching": true
    }
}
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Curl" %}

```bash
curl --request GET 'https://sandbox.cardscan.ai/v1/ping'
```

{% endtab %}
{% endtabs %}

### Authenticated Health Check

<mark style="color:blue;">`GET`</mark> `https://sandbox.cardscan.ai/v1/authping`

An authenticated health check endpoint that verifies your API key or session token is valid and returns service status.

{% tabs %}
{% tab title="200 Successful Response" %}

```json
{
    "timestamp": "2024-02-05T15:50:14.856792Z",
    "authenticated": true,
    "account_id": "D05BD263-CC9E-437D-9AEE-9846196F18BA"
}
```

{% endtab %}

{% tab title="401 Missing or invalid token" %}

```json
{
    "message": "Unknown authorization error - please check your token format and try again",
    "type": "Unauthorized",
    "code": 401
}
```

{% endtab %}

{% tab title="403 Token is expired" %}

```json
{
    "message": "Token is expired",
    "type": "Authentication Timeout",
    "code": 419
}
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Curl" %}

```bash
curl --request GET 'https://sandbox.cardscan.ai/v1/authping' \
--header 'Authorization: Bearer sk_test_cardscan_ai_XXXXXXXXXXXXXXX'
```

{% endtab %}
{% endtabs %}


# React ⚛

Our `CardScanView` react component makes it easy to add insurance card scanning and eligibility verification to any web application in **5 minutes or less.**

![](/files/gNDYeSz9NlEnY6uwVEel)

### Installation

```
$ npm i @cardscan.ai/insurance-cardscan-react
$ yarn add @cardscan.ai/insurance-cardscan-react
```

### Usage

Import the library widget and model into your project files:

```javascript
import { CardScanView, CardScanModel } from "@cardscan.ai/insurance-cardscan-react";
```

You can **optionally** add the API client for more custom applications.

```javascript
import { CardScanApi } from "@cardscan.ai/cardscan-client";
```

### Basic Example

```jsx
import React from "react";
import { render } from "react-dom";
import { CardScanView, CardScanModel } from "@cardscan.ai/insurance-cardscan-react";

function onSuccess(card: any) {
  console.log("New Card: ", card);
}

function onError(error) {
  console.error('Error occurred: ', error);
}

// See Authentication on where to get this sessionToken.
const sessionToken = 'JWT_TOKEN'

// Initializes and warms the model. See CardScanModel for more info.
useEffect(() => {
  CardScanModel.warm();
}, []);

// Render CardScanView
function App() {
  return (
    <div className="App">
      <div className="CardScanContainer">
        <CardScanView
          live={false}
          sessionToken={sessionToken}
          onSuccess={onSuccess}
          onError={onError}
        />
      </div>
    </div>
  );
}

const rootElement = document.getElementById('root');
render(<App />, rootElement);

```

### Compatibility

* **Node Versions**: 14, 16, 18
* **React Versions**: >=17.0.2, 18.2.0+
* **Webpack Versions**: 4.x, 5.x
* **Babel Versions**: 6.x, 7.x

### Peer Dependencies

* **React**: >=17.0.2
* **React-DOM**: >=17.0.2

## Configuration

### Available Props

```jsx
<CardScanView
  // Required
  live={false}
  sessionToken={token}
  onSuccess={onSuccess}

  // Recommneded
  onCancel={onCancel}
  onError={onError}
  onProgress={onProgress}
  
  // Eligibility
  eligibility={eligibility}
  onEligibilitySuccess={eligibilitySuccess}
  onEligibilityError={eligibilityError}
  
  // Camera Options
  cameraOptions={cameraOptions}
  
  // Optional
  backsideSupport={scanBackside}
  onRetry={onRetry}
  cameraPermissionModalConfig={cameraPermissionModalConfig}
  webToMobileHandoffConfig={webToMobileHandoffConfig}

  // UI Customization
  messages={messages}
  successIndicator={successIndicator}
  errorIndicator={errorIndicator}
  closeButton={closeButton}
/>
```

### **Main Props**

<table><thead><tr><th width="289">Prop</th><th width="112">Required</th><th width="135">Type</th><th>Description</th></tr></thead><tbody><tr><td>live</td><td>false</td><td>Boolean</td><td>Toggle the production or sandbox version. <strong>Default</strong>: false</td></tr><tr><td>sessionToken</td><td>true</td><td>String</td><td>A JWT token for the current user, see <a href="/pages/-MfnTIPuqKcHV1TvM04w">Authentication</a></td></tr><tr><td>onSuccess</td><td>true</td><td>Function</td><td>Called on successful scan. The first argument is the scanned card.</td></tr><tr><td>onCancel</td><td>false</td><td>Function</td><td>Triggered when the user cancels the CardScanView UI.</td></tr><tr><td>onError</td><td>false</td><td>Function</td><td>Called when an error is returned by the API or the CardScanView fails to initialize.</td></tr><tr><td>onProgress</td><td>true</td><td>Function</td><td>Progress updates during the card scanning process.</td></tr><tr><td>onRetry</td><td>false</td><td>Function</td><td>Called when a failed scan triggers a retry.</td></tr><tr><td>fullScreen</td><td>false</td><td>Boolean</td><td>Toggles the widget between full viewport coverage and a 16:9 aspect ratio within the parent container's width.<br><strong>Default:</strong> true</td></tr><tr><td>backsideSupport</td><td>false</td><td>Boolean</td><td>Enable scanning of the front and back side of the card.<br><strong>Default</strong>: false</td></tr><tr><td>eligibility</td><td>false</td><td>object</td><td>Request payload for the optional post-scan eligibility verification. See: <a data-mention href="/pages/F8HfWCgSF0hYgQuWMlRS">/pages/F8HfWCgSF0hYgQuWMlRS</a></td></tr><tr><td>onEligibilitySuccess</td><td>false</td><td>Function</td><td>Called on successful eligibility request. The eligibility response is pass as an argument.</td></tr><tr><td>onEligibilityError</td><td>false</td><td>Function</td><td>Called when an error is returned by the eligibility API.</td></tr><tr><td>cameraPermissionModalConfig</td><td>false</td><td>object</td><td>The <a href="#camerapermissionmodalconfig"><code>cameraPermissionModalConfig</code></a> object allows customization of the Camera Permission Modal in the application. It provides the flexibility to set custom texts for various parts of the modal, enhancing the user experience by providing clear and relevant information.<br>Default :<code>{ enabled: true }</code></td></tr><tr><td>webToMobileHandoffConfig</td><td>false</td><td>object</td><td>Enables seamless handoff from desktop to mobile when errors are encountered. See: <a data-mention href="/pages/7fxxgDKsQzcSkAmd0ySk">/pages/7fxxgDKsQzcSkAmd0ySk</a></td></tr><tr><td>cameraOptions</td><td>false</td><td>object</td><td>Configures the camera's orientation during scanning.</td></tr></tbody></table>

## UI/UX Customization

The react widget is designed to be highly customizable. Please see the [#customization](#customization "mention") section of UI Components to adjust these elements to match your application's branding and theme:

[Customization ⚙️](/ui-components/customization)

{% hint style="info" %}
**Note:** All UI/UX Props are optional
{% endhint %}

<table><thead><tr><th width="313.3333333333333">Prop</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>messages</td><td>Object</td><td>Customize the text displayed by the UI.</td></tr><tr><td>successIndicator</td><td>ReactNode</td><td>Replace the success indicator with a custom react component.</td></tr><tr><td>errorIndicator</td><td>ReactNode</td><td>Replace the error indicator with a custom react component.</td></tr><tr><td>closeButton</td><td>ReactNode</td><td>Replace the close button with a custom react component.</td></tr></tbody></table>

{% hint style="success" %}
**Note:** Additional UI elements can be modified using CSS
{% endhint %}

## Callbacks

### onSuccess Callback

The `onSuccess` prop allows you to execute a custom function when the card scanning process is completed successfully. This function receives the scanned card data as an argument.

#### Usage

To use the `onSuccess` prop, pass a function that receives the scanned card data:

```jsx
const handleCardScanSuccess = (cardData) => {
  console.log('Card scanned successfully:', cardData);
};

<CardScanView
  sessionToken={token}
  onSuccess={handleCardScanSuccess}
/>
```

In this example, the `handleCardScanSuccess` function logs the scanned card data to the console when the scanning process is completed successfully.

### onError Callback

The `onError` prop allows you to execute a custom function when there's a failure during the card scanning process. This function receives an error object as an argument.

#### Usage

To use the `onError` prop, pass a function that receives the error object:

```jsx
const handleCardScanError = (error: CardScanError) => {
  console.error('Scanning failed:', error);
};

<CardScanView
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onError={handleCardScanError}
/>
```

In this example, the `handleCardScanError` function logs the error object to the console when a scanning failure occurs.

By using the `onSuccess` and `onError` props, you can handle successful and failed scanning events, allowing you to implement custom actions or display appropriate messages to the user.

{% hint style="info" %}
You can find examples of the[#error-screens](#error-screens "mention") at the bottom of this document
{% endhint %}

Possible errors returned to the `onError` callback.

```typescript
type CardScanError = {
  message: string;
  type: string;
  code: number;
  isCardScanError: boolean;
};
```

<table><thead><tr><th width="235.66666666666666">Error Type</th><th>Error Code</th><th>Error Message</th></tr></thead><tbody><tr><td>VideoError</td><td>670</td><td>Various system errors of <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia">getUserMedia</a>(), including: “Permission Denied” and “Camera not found”</td></tr><tr><td>VideoError</td><td>675</td><td>Various system errors of <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play">HTMLMediaElement.play()</a>, including: “Permission Denied” and “The element has no supported sources.”</td></tr><tr><td>VideoError</td><td>676</td><td>Any and all other system related video capture &#x26; canvas capture errors.</td></tr><tr><td>WSError</td><td>640</td><td>“No websocket found - critical failure”</td></tr><tr><td>WSError</td><td>642</td><td>“Unknown error from websocket”</td></tr><tr><td>WSError</td><td><a href="https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code">1002-1003, 1007-1015</a></td><td>“The connection was closed abnormally”</td></tr><tr><td>ResponseError</td><td>HTTP Codes</td><td>Various HTTP errors returned from XMLHttpRequest and the CardScan.ai backend.</td></tr><tr><td>RequestError</td><td>Various</td><td>Various HTTP errors returned from Axios and XMLHttpRequest.</td></tr><tr><td>Unknown</td><td>606</td><td>Possible Axios setup errors, websocket setup errors, etc.</td></tr></tbody></table>

### onCancel Callback

The `onCancel` prop enables you to execute a custom function when the user cancels the card scanning process. This can be useful for tracking user behavior, navigating to a different part of the application, or displaying an appropriate message.

#### Usage

To use the `onCancel` prop, pass a function that will be executed when the user cancels the scanning process:

```jsx
import React, { useState } from 'react';
import {CardScanView} from "@cardscan.ai/insurance-cardscan-react";

const App = () => {
  const [showCardScanView, setShowCardScanView] = useState(true);

  return (
    <div>
      {showCardScanView && (
        <CardScanView
          sessionToken={token}
          onSuccess={cardScanSuccess}
          onError={cardScanError}
          onCancel={() => setShowCardScanView(false)}
        />
      )}
    </div>
  );
};

export default App;

```

In this example, we initialize the `showCardScanView` state variable to `true`. Then, we conditionally render the `CardScanView` component based on the value of `showCardScanView`. We then use the `onCancel` prop to set the `showCardScanView` state variable to `false` when the user cancels the scanning process.

### onRetry Callback

The `onRetry` prop allows you to execute a custom function when a retry is triggered due to a scanning failure.

#### Usage

To use the `onRetry` prop, pass a function that receives the retry event as an argument:

```jsx
const handleRetry = (event) => {
  console.log('Retry triggered:', event);
};

<CardScanView
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onRetry={handleRetry}
/>
```

In this example, the `handleRetry` function logs the retry event to the console when a retry is triggered.

### onProgress Callback

The `onProgress` prop allows you to execute a custom function to report progress during the scanning process.

#### Usage

```jsx
const handleProgress = (progress) => {
  console.log(`Card ID: ${progress.cardId} - Scan Count: ${progress.scanCount} - Card State: ${progress.cardState}`);
};

<CardScanView
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onProgress={handleProgress}
/>
```

In this example, the `handleProgress` function logs the progress during a live scanning operation. When `backsideSupport` is enabled the scan counter will reset to zero when the card is flipped to scan the back side.

{% hint style="info" %}
For the majority of live scanning scenarios, the **scanning will be completed within 2-3 scans.** In situations with low light or occluded card elements, the scan count can go as high as 12.
{% endhint %}

## Additional Features

### Eligibility Verification

Eligibility Verification is crucial in healthcare for confirming a patient's insurance coverage and benefits before services are provided, streamlining billing and enhancing patient care.

By providing subscriber and provider details through our React component, users can initiate the verification process effortlessly. The widget offers real-time feedback with success and error callbacks, simplifying integration into your application's UI.

See [Eligibility Verification 🩻](/advanced-features/eligibility-verification) for more details.

### Embedding

The `fullScreen` prop controls the sizing behavior of the widget. When set to `true`, the widget expands to fill the entire viewport, providing a full-screen experience. Setting it to `false` keeps the widget within the width of its parent container, maintaining a 16:9 aspect ratio. This property ensures that the widget can be embedded in various layouts while preserving its visual integrity.

**Example:**

```jsx
<div className="scanner-container" style={{ width: "800px" }}>
    <CardScanView 
        fullScreen={false}
        sessionToken={token}
        onSuccess={cardScanSuccess}
        onCancel={cardScanCancel}
        onError={cardScanError}
    />
</div>
```

### Camera Options

Camera Configuration allow developers to preset the camera's view orientation for specific installation scenarios, such as mirrored cameras or kiosks. These settings ensure the camera view is correctly aligned and oriented during the initial setup of the application:

* `flipHorizontal`: Enables horizontal flipping of camera view, reversing the left to right sides of the image (aka mirrored)
* `flipVertical`: Enabled vertical flipping of camera view, swapping the top and bottom of the image when the camera or document is upside down.

```jsx
<CardScanView 
    sessionToken={token}
    onSuccess={cardScanSuccess}
    cameraOptions={{
        flipHorizontal: false,
        flipVertical: true
    }}
/>

```

### Camera Permissions Modal

The camera permission modal is designed to handle all possible scenarios where users may refuse permission for their camera device. It serves to guide users through the steps needed to resolve a rejected permission request, whether the user manually declined access or if the device automatically blocked the permission. This ensures that users are informed and can take the necessary actions to enable camera access.

For further details, please visit the dedicated page:

{% content-ref url="/pages/O5kK0cj4UwgSPuKrXyF8" %}
[Camera Permission Modal 📸](/advanced-features/camera-permission-modal)
{% endcontent-ref %}

### CardScanModel

The `CardScanModel` class is designed to load and "warm" the TensorFlowJS model used during live scanning to detect insurance cards. Properly warming the model ensures it is ready for immediate use in the scanning process.

#### Importing the class

To utilize the `CardScanModel` in your project, import it as follows:

```tsx
import { CardScanModel } from "@cardscan.ai/insurance-cardscan-react";
```

#### **Warming the Model**

While importing the class and warming the model are optional, doing so will result in faster widget initialization and happier users. To warm the model, call the `warm` method once during the component mount. This initializes the model loading and warming process, making it ready for scanning.

```tsx
CardScanModel.warm();
```

**Custom Model Path**

Optionally, you can provide a custom model path to a local version of the model file or to a CDN that you control. Ensure the model files are included in your build folder. If the model cannot be loaded from the provided path, it will default to fetching the model from the CDN.

```tsx
CardScanModel.warm("path/to/model.json");
```

#### Model Assets

To include the model assets in your build system, configure your bundler (such as Webpack, Rollup, or a similar tool) to import all files from the `assets/model-tfjs` folder in the npm package. The exact configuration will depend on your specific build setup and requirements.

{% hint style="info" %}
Please reach out to support if you need help configuring your build system.
{% endhint %}

## Error Screens

#### **Setup Failure**

<figure><img src="/files/XBK57FQU7zDvr0JZm5Yy" alt=""><figcaption></figcaption></figure>

This screen will be shown on setup connection failure (websocket or initial REST API call), network error, auth failure (invalid or expired token), and camera error (without webToMobileHandOff or enableCameraPermissionModal enabled).

#### **Scan Failure**

<figure><img src="/files/MvTTyH9VFc2JDPPUOR4f" alt=""><figcaption></figcaption></figure>

This screen will be shown on direct upload of an invalid image (i.e. a photo of a cat) or when all 12+ scan attempts have been exhausted because of poor input quality or complete backend failure.

{% hint style="info" %}
**Are we missing something?** Please let us know and we would be happy to add it. Contact [support](mailto:support@cardscan.ai).
{% endhint %}


# React DropZone 📤

The `CardScanDropZone` component provides an alternative to camera-based scanning by offering a **drag-and-drop file upload interface** for insurance card processing. This component is perfect for desktop workflows where users have digital images of their insurance cards ready to upload.

### Installation

```bash
$ npm i @cardscan.ai/insurance-cardscan-react
$ yarn add @cardscan.ai/insurance-cardscan-react
```

### Usage

Import the component into your project:

```javascript
import { CardScanDropZone } from "@cardscan.ai/insurance-cardscan-react";
```

### Basic Example

```jsx
import React from "react";
import { render } from "react-dom";
import { CardScanDropZone } from "@cardscan.ai/insurance-cardscan-react";

function onSuccess(card) {
  console.log("Card processed successfully:", card);
}

function onError(error) {
  console.error('Error occurred:', error);
}

// See Authentication on where to get this sessionToken
const sessionToken = 'JWT_TOKEN'

function App() {
  return (
    <div className="App">
      <div className="DropZoneContainer">
        <CardScanDropZone
          live={false}
          sessionToken={sessionToken}
          onSuccess={onSuccess}
          onError={onError}
          enableBackside={true}
          layout="side-by-side"
        />
      </div>
    </div>
  );
}

const rootElement = document.getElementById('root');
render(<App />, rootElement);
```

### Compatibility

* **Node Versions**: 14, 16, 18
* **React Versions**: >=17.0.2, 18.2.0+
* **Webpack Versions**: 4.x, 5.x
* **Babel Versions**: 6.x, 7.x

### Peer Dependencies

* **React**: >=17.0.2
* **React-DOM**: >=17.0.2
* **react-dropzone**: ^14.0.0

## Configuration

### Available Props

```jsx
<CardScanDropZone
  // Required
  live={false}
  sessionToken={token}
  onSuccess={onSuccess}

  // Recommended
  onCancel={onCancel}
  onError={onError}
  onProgress={onProgress}
  
  // Dropzone Options
  enableBackside={true}
  layout="side-by-side"
  
  // UI Customization
  messages={messages}
  frontDropIndicator={frontDropIndicator}
  backDropIndicator={backDropIndicator}
  successIndicator={successIndicator}
  errorIndicator={errorIndicator}
  
  // Styling
  widgetBackgroundColor="#ffffff"
  primaryColor="#007bff"
  progressBarColor="#28a745"
/>
```

### Main Props

| Prop           | Required | Type     | Description                                                                      |
| -------------- | -------- | -------- | -------------------------------------------------------------------------------- |
| live           | true     | Boolean  | Toggle production or sandbox version. **Default**: false                         |
| sessionToken   | true     | String   | JWT token for the current user, see [Authentication](/authentication)            |
| onSuccess      | true     | Function | Called on successful card processing. First argument is the processed card data. |
| onCancel       | false    | Function | Triggered when user cancels the upload process.                                  |
| onError        | false    | Function | Called when an error occurs during upload or processing.                         |
| onProgress     | false    | Function | Progress updates during card processing.                                         |
| enableBackside | false    | Boolean  | Enable uploading of both front and back card sides. **Default**: false           |
| layout         | false    | String   | Layout mode: `"side-by-side"` or `"sequential"`. **Default**: `"side-by-side"`   |

### Layout Options

The `layout` prop controls how the front and back upload zones are displayed:

#### Side-by-Side Layout

```jsx
<CardScanDropZone
  layout="side-by-side"
  enableBackside={true}
  // ... other props
/>
```

Both drop zones are visible simultaneously, allowing users to see both upload areas at once.

#### Sequential Layout

```jsx
<CardScanDropZone
  layout="sequential"
  enableBackside={true}
  // ... other props
/>
```

The back drop zone is hidden until the front card is successfully uploaded and processed.

## File Requirements

The CardScanDropZone component enforces the following file requirements:

* **File Types**: JPEG, PNG
* **File Size**: Maximum 5MB per file
* **Image Quality**: Clear, well-lit images for best OCR results

## Upload Workflow

1. **Front Card Upload**: User drags and drops or clicks to select the front card image
2. **File Validation**: Component validates file type and size
3. **Upload & Processing**: File is uploaded via secure S3 direct upload and processed
4. **Real-time Updates**: WebSocket connection provides live processing status
5. **Back Card (Optional)**: If `enableBackside` is true, back card upload becomes available
6. **Completion**: `onSuccess` callback is triggered with complete card data

## UI Customization

### Custom Messages

```jsx
const customMessages = {
  frontDropTitle: "Upload Front of Insurance Card",
  frontDropSubtitle: "Drag and drop or click to select",
  backDropTitle: "Upload Back of Insurance Card", 
  backDropSubtitle: "Drag and drop or click to select",
  frontProcessingTitle: "Processing Front Card...",
  backProcessingTitle: "Processing Back Card...",
  uploadingTitle: "Uploading...",
  successTitle: "Upload Complete!",
  errorTitle: "Upload Failed",
  fileTooLargeError: "File size must be under 5MB",
  invalidFileTypeError: "Please upload a JPEG or PNG file"
};

<CardScanDropZone
  messages={customMessages}
  // ... other props
/>
```

### Custom Components

Replace default UI elements with your own React components:

```jsx
const CustomFrontIndicator = () => (
  <div className="custom-drop-indicator">
    <h3>📄 Drop Front Card Here</h3>
  </div>
);

const CustomSuccessIndicator = () => (
  <div className="custom-success">
    <h3>✅ Card Processed Successfully!</h3>
  </div>
);

<CardScanDropZone
  frontDropIndicator={<CustomFrontIndicator />}
  successIndicator={<CustomSuccessIndicator />}
  // ... other props
/>
```

### CSS Styling

The component supports CSS custom properties for easy theming:

```css
.cardscan-dropzone {
  --dropzone-bg-color: #f8f9fa;
  --dropzone-border-color: #dee2e6;
  --dropzone-text-color: #212529;
  --dropzone-primary-color: #007bff;
  --dropzone-success-color: #28a745;
  --dropzone-error-color: #dc3545;
  --dropzone-border-radius: 8px;
}
```

Or use the styling props:

```jsx
<CardScanDropZone
  widgetBackgroundColor="#ffffff"
  primaryColor="#007bff"
  progressBarColor="#28a745"
  // ... other props
/>
```

## State Management

The CardScanDropZone uses XState for robust state management with the following states:

* **Initial**: Ready for front card upload
* **FrontUploading**: Front card being uploaded
* **FrontProcessing**: Front card being processed by ML pipeline
* **BackAvailable**: Ready for back card upload (if enabled)
* **BackUploading**: Back card being uploaded
* **BackProcessing**: Back card being processed
* **Success**: Both cards processed successfully
* **Error**: Upload or processing failed

## Callbacks

### onSuccess Callback

```jsx
const handleSuccess = (cardData) => {
  console.log('Processed card data:', cardData);
  // cardData contains extracted information from both front and back
};

<CardScanDropZone
  onSuccess={handleSuccess}
  // ... other props
/>
```

### onError Callback

```jsx
const handleError = (error) => {
  console.error('Upload error:', error);
  // Error object contains type, message, and code
};

<CardScanDropZone
  onError={handleError}
  // ... other props
/>
```

### onProgress Callback

```jsx
const handleProgress = (progress) => {
  console.log(`Processing: ${progress.cardSide} - State: ${progress.state}`);
};

<CardScanDropZone
  onProgress={handleProgress}
  // ... other props
/>
```

## Error Handling

The component handles various error scenarios:

| Error Type          | Description             | User Action               |
| ------------------- | ----------------------- | ------------------------- |
| FileTooLarge        | File exceeds 5MB limit  | Select a smaller file     |
| InvalidFileType     | File is not JPEG/PNG    | Select a valid image file |
| UploadFailed        | Network or server error | Try uploading again       |
| ProcessingFailed    | ML processing failed    | Try with a clearer image  |
| AuthenticationError | Invalid session token   | Refresh authentication    |

## Integration with CardScanView

The DropZone component can be used alongside the camera-based CardScanView:

```jsx
const [uploadMethod, setUploadMethod] = useState('camera');

return (
  <div>
    <div className="method-selector">
      <button onClick={() => setUploadMethod('camera')}>
        📸 Use Camera
      </button>
      <button onClick={() => setUploadMethod('upload')}>
        📤 Upload Files
      </button>
    </div>
    
    {uploadMethod === 'camera' ? (
      <CardScanView
        sessionToken={sessionToken}
        onSuccess={onSuccess}
        onError={onError}
      />
    ) : (
      <CardScanDropZone
        sessionToken={sessionToken}
        onSuccess={onSuccess}
        onError={onError}
        enableBackside={true}
      />
    )}
  </div>
);
```

## Best Practices

1. **File Validation**: Always handle file validation errors gracefully
2. **User Feedback**: Provide clear visual feedback during upload and processing
3. **Error Recovery**: Allow users to retry failed uploads easily
4. **Accessibility**: Ensure keyboard navigation and screen reader support
5. **Mobile Considerations**: Consider using CardScanView for mobile devices where camera access is preferred

## Troubleshooting

### Common Issues

**Files not uploading:**

* Check session token validity
* Verify file size is under 5MB
* Ensure file format is JPEG or PNG

**Processing takes too long:**

* Images with poor quality may require more processing time
* Consider implementing a timeout mechanism

**Styling not applying:**

* Ensure CSS custom properties have sufficient specificity
* Check for conflicting styles from other components

{% hint style="info" %}
**Need Help?** Contact <support@cardscan.ai> for assistance with the CardScanDropZone component.
{% endhint %}


# React Native ⚛

Our `CardScanView` react component makes it easy to add insurance card scanning to any React Native application in **5 minutes or less.**

### Installation

```
$ npm i @cardscan.ai/insurance-cardscan-react-native
$ yarn add @cardscan.ai/insurance-cardscan-react-native
```

### Usage

Import the library widget into your project files:

```javascript
import { CardScanView } from "@cardscan.ai/insurance-cardscan-react-native";
```

### Basic Example

```jsx
import React from 'react';
import { SafeAreaView, StyleSheet } from 'react-native';
import { CardScanView } from '@cardscan.ai/insurance-cardscan-react-native';

function onSuccess(card: any) {
  console.log('new card: ', card);
}

function onError(error) {
  console.error('Error occurred: ', error);
}

// See Authentication on where to get this sessionToken.
const sessionToken = 'JWT_TOKEN';

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <CardScanView 
        live={false} 
        sessionToken={sessionToken} 
        onSuccess={onSuccess} 
        onError={onError}
      />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

```

### Available Props

```jsx
<CardScanView
  // Required
  live={false}
  sessionToken={token}
  onSuccess={onSuccess}

  // Recommneded
  onCancel={onCancel}
  onError={onError}
  
  // Optional
  backsideSupport={scanBackside}
  onRetry={onRetry}

  // UI Customization
  messages={messages}
  messageStyle={messageStyle}
  autoSwitchActiveColor={autoSwitchActiveColor}
  autoSwitchInactiveColor={autoSwitchInactiveColor}
  progressBarColor={progressBarColor}
  widgetBackgroundColor={widgetBackgroundColor}
/>
```

### **Main Props**

<table><thead><tr><th width="289">Prop</th><th width="112">Required</th><th width="135">Type</th><th>Description</th></tr></thead><tbody><tr><td>live</td><td>false</td><td>Boolean</td><td>Toggle the production or sandbox version. <strong>Default</strong>: false</td></tr><tr><td>sessionToken</td><td>true</td><td>String</td><td>A JWT token for the current user, see <a href="/pages/-MfnTIPuqKcHV1TvM04w">Authentication</a></td></tr><tr><td>onSuccess</td><td>true</td><td>Function</td><td>Called on successful scan. The first argument is the scanned card.</td></tr><tr><td>onCancel</td><td>false</td><td>Function</td><td>Triggered when the user cancels the CardScanView UI.</td></tr><tr><td>onError</td><td>false</td><td>Function</td><td>Called when an error is returned by the API or the CardScanView fails to initialize.</td></tr><tr><td>backsideSupport</td><td>false</td><td>Boolean</td><td>Enable scanning of the front and back side of the card.<br><strong>Default</strong>: false</td></tr><tr><td>onRetry</td><td>false</td><td>Function</td><td>Called when a failed scan triggers a retry.</td></tr></tbody></table>

### UI/UX Customization Props

The react widget is designed to be highly customizable. Please see the [#customization](#customization "mention") section of UI Components to adjust these elements to match your application's branding and theme:

[Customization ⚙️](/ui-components/customization)

{% hint style="info" %}
**Note:** All UI/UX Props are optional
{% endhint %}

<table><thead><tr><th width="329.3333333333333">Prop</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>messages</td><td>Object</td><td>Customize the text displayed by the UI.</td></tr><tr><td>messageStyle</td><td>Object</td><td>Set the size, color and background color of the text displayed by the UI.</td></tr><tr><td>autoSwitchActiveColor</td><td>String</td><td>Set the color of the auto scan switch</td></tr><tr><td>autoSwitchInactiveColor</td><td>String</td><td>Set the color of the disabled auto scan switch</td></tr><tr><td>progressBarColor</td><td>String</td><td>Set the color of the progress bars or bounding box that surrounds the card scanning area.</td></tr><tr><td>widgetBackgroundColor</td><td>String</td><td>Set the main background color for the widget.</td></tr></tbody></table>

### onSuccess Callback

The `onSuccess` prop allows you to execute a custom function when the card scanning process is completed successfully. This function receives the scanned card data as an argument.

#### Usage

To use the `onSuccess` prop, pass a function that receives the scanned card data:

```jsx
const handleCardScanSuccess = (cardData) => {
  console.log('Card scanned successfully:', cardData);
};

<CardScanView
  sessionToken={token}
  onSuccess={handleCardScanSuccess}
/>
```

In this example, the `handleCardScanSuccess` function logs the scanned card data to the console when the scanning process is completed successfully.

### onError Callback

The `onError` prop allows you to execute a custom function when there's a failure during the card scanning process. This function receives an error object as an argument.

#### Usage

To use the `onError` prop, pass a function that receives the error object:

```jsx
const handleCardScanError = (error) => {
  console.error('Scanning failed:', error);
};

<CardScanView
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onError={handleCardScanError}
/>
```

In this example, the `handleCardScanError` function logs the error object to the console when a scanning failure occurs.

By using the `onSuccess` and `onError` props, you can handle successful and failed scanning events, allowing you to implement custom actions or display appropriate messages to the user.

### onCancel Callback

The `onCancel` prop enables you to execute a custom function when the user cancels the card scanning process. This can be useful for tracking user behavior, navigating to a different part of the application, or displaying an appropriate message.

#### Usage

To use the `onCancel` prop, pass a function that will be executed when the user cancels the scanning process:

```jsx
import React, { useState } from 'react';
import { View } from 'react-native';
import { CardScanView } from '@cardscan.ai/insurance-cardscan-react-native';



const App = () => {
  const [showCardScanView, setShowCardScanView] = useState(true);

  return (
    <View>
      {showCardScanView && (
        <CardScanView
          sessionToken={token}
          onSuccess={cardScanSuccess}
          onError={cardScanError}
          onCancel={() => setShowCardScanView(false)}
        />
      )}
    </View>
  );
};

export default App;

```

In this example, we initialize the `showCardScanView` state variable to `true`. Then, we conditionally render the `CardScanView` component based on the value of `showCardScanView`. We then use the `onCancel` prop to set the `showCardScanView` state variable to `false` when the user cancels the scanning process.

### onRetry Callback

The `onRetry` prop allows you to execute a custom function when a retry is triggered due to a scanning failure.

#### Usage

To use the `onRetry` prop, pass a function that receives the retry event as an argument:

```jsx
const handleRetry = (event) => {
  console.log('Retry triggered:', event);
};

<CardScanView
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onRetry={handleRetry}
/>
```

In this example, the `handleRetry` function logs the retry event to the console when a retry is triggered.


# Flutter 🦅

Our `CardScan` Flutter widget makes it easy to add insurance card scanning to any Flutter application in 5 minutes or less.

### Installation

Add the `insurance_card_scanner` package to your `pubspec.yaml` file.

```yaml
dependencies:
  flutter:
    sdk: flutter
  insurance_card_scanner: ^0.3.0
```

### Usage

Import the library widget into your project files:

```dart
import 'package:insurance_card_scanner/insurance_card_scanner.dart';
```

You can **optionally** add the API client for more custom applications.

```dart
import 'package:insurance_card_scanner/insurance_card_scanner_api.dart';
```

### Basic Example

```dart
import 'package:insurance_card_scanner/insurance_card_scanner.dart';

class ScannerWidgetScreen extends StatelessWidget {
  const ScannerWidgetScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Scanner widget'),
      ),
      body: CardScanner(
        properties: CardScanConfig(
          sessionToken: '<pass your token here>',
          onSuccess: (card) {
            print('Scan success');
          },
          onError: (message) {
            print(message ?? 'Unknown Scan error');
          },
          onCancel: () {
            Navigator.of(context).pop();
          },
        ),
      ),
    );
  }
}
```

### Available Properties

Both `CardScanner` and `CardScannerModal` should be passed a `CardScanConfig` instance with properties for server connection, callback handling and UI customization.

```dart
CardScanConfig(
  // Required
  sessionToken: token,
  live: false,
  onSuccess: onSuccess,

  // Recommended
  onCancel: onCancel,
  onError: onError,

  // Optional
  backsideSupport: scanBackside,
  onRetry: onRetry,

  // UI Customization
  messages: messages,
  messageStyle: messagesStyle,
  autoSwitchActiveColor: autoSwitchActiveColor,
  autoSwitchInactiveColor: autoSwitchInactiveColor,
  progressBarColor: progressBarColor,
  widgetBackgroundColor: widgetBackgroundColor,
)

```

### **Main Props**

<table><thead><tr><th width="289">Prop</th><th width="112">Required</th><th width="135">Type</th><th>Description</th></tr></thead><tbody><tr><td>live</td><td>false</td><td>Boolean</td><td>Toggle the production or sandbox version. <strong>Default</strong>: false</td></tr><tr><td>sessionToken</td><td>true</td><td>String</td><td>A JWT token for the current user, see <a href="/pages/-MfnTIPuqKcHV1TvM04w">Authentication</a></td></tr><tr><td>onSuccess</td><td>true</td><td>Function</td><td>Called on successful scan. The first argument is the scanned card.</td></tr><tr><td>onCancel</td><td>false</td><td>Function</td><td>Triggered when the user cancels the CardScanner UI.</td></tr><tr><td>onError</td><td>false</td><td>Function</td><td>Called when an error is returned by the API or the CardScanner fails to initialize.</td></tr><tr><td>backsideSupport</td><td>false</td><td>Boolean</td><td>Enable scanning of the front and back side of the card.<br><strong>Default</strong>: false</td></tr><tr><td>onRetry</td><td>false</td><td>Function</td><td>Called when a failed scan triggers a retry.</td></tr></tbody></table>

### UI/UX Customization Props

The flutter widget is designed to be customizable. Please see the [#customization](#customization "mention") section of UI Components to adjust these elements to match your application's branding and theme:

[Customization ⚙️](/ui-components/customization)

{% hint style="info" %}
**Note:** All UI/UX Props are optional
{% endhint %}

<table><thead><tr><th width="329.3333333333333">Prop</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>messages</td><td>Object</td><td>Customize the text displayed by the UI.</td></tr><tr><td>messageStyle</td><td>TextStyle</td><td>Set the size, color and background color of the text displayed by the UI.</td></tr><tr><td>autoSwitchActiveColor</td><td>Color</td><td>Set the color of the auto scan switch</td></tr><tr><td>autoSwitchInactiveColor</td><td>Color</td><td>Set the color of the disabled auto scan switch</td></tr><tr><td>progressBarColor</td><td>Color</td><td>Set the color of the progress bars or bounding box that surrounds the card scanning area.</td></tr><tr><td>widgetBackgroundColor</td><td>Color</td><td>Set the main background color for the widget.</td></tr></tbody></table>

### onSuccess Callback

The `onSuccess` callback allows you to execute a custom function when the card scanning process is completed successfully. This function receives the scanned card data as an argument.

#### Usage

To use the `onSuccess` callback, pass a function that receives the scanned card data:

```dart
void handleCardScanSuccess(Card card) {
  print('Card scanned successfully: $card');
}

CardScanner(
  sessionToken: token,
  onSuccess: handleCardScanSuccess,
)
```

In this example, the `handleCardScanSuccess` function logs the scanned card data to the console when the scanning process is completed successfully.

###

### onError Callback

The `onError` callback allows you to execute a custom function when there's a failure during the card scanning process. This function receives an error object as an argument.

#### Usage

To use the `onError` callback, pass a function that receives the error object:

```dart
void handleCardScanError(CardScanError error) {
  print('Scanning failed: $error');
}

CardScanner(
  sessionToken: token,
  onSuccess: handleCardScanSuccess,
  onError: handleCardScanError,
)

```

In this example, the `handleCardScanError` function logs the error object to the console when a scanning failure occurs.

By using the `onSuccess` and `onError` callbacks, you can handle successful and failed scanning events, allowing you to implement custom actions or display appropriate messages to the user.

### onCancel Callback

The `onCancel` callback enables you to execute a custom function when the user cancels the card scanning process. This can be useful for tracking user behavior, navigating to a different part of the application, or displaying an appropriate message.

#### Usage

To use the `onCancel` callback, pass a function that will be executed when the user cancels the scanning process:

```dart
import 'package:flutter/material.dart';
import 'package:insurance_card_scanner/insurance_card_scanner.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool showCardScanWidget = true;

  void handleCardScanSuccess(Card card) {
    // Handle card scan success
  }

  void handleCardScanError(CardScanError error) {
    // Handle card scan error
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Insurance Card Scanner'),
        ),
        body: showCardScanWidget
            ? CardScanner(
                sessionToken: token,
                onSuccess: handleCardScanSuccess,
                onError: handleCardScanError,
                onCancel: () => setState(() => showCardScanWidget = false),
              )
            : Container(),
      ),
    );
  }
}

```

In this example, we initialize the `showCardScanWidget` state variable to `true`. Then, we conditionally render the `CardScanner` widget based on the value of `showCardScanWidget`. We then use the `onCancel` callback to set the `showCardScanWidget` state variable to `false` when the user cancels the scanning process.

### onRetry Callback

The `onRetry` callback allows you to execute a custom function when a retry is triggered due to a scanning failure.

#### Usage

```dart
void handleRetry() {
  print('Retry triggered');
}

CardScanner(
  sessionToken: token,
  onSuccess: handleCardScanSuccess,
  onRetry: handleRetry,
)
```

In this example, the `handleRetry` function logs the retry event to the console when a retry is triggered.

### Camera Permissions 📸

Our SDK requires camera access to scan insurance cards. While the CardScan widget automatically requests camera permissions during widget load, it does not present any UI for handling permissions or manage permission failures, particularly on mobile devices.

**Requirement:** We require that developers handle camera permission requests ***before*** loading the CardScan widget. This will allow you to manage the permission flow consistently within your application, providing custom error handling and user feedback if permission is denied.

**Recommendation:** We recommend following best practices when requesting camera permissions:

* **Pre-flight the request:**

  Within your app, prompt the user with a clear explanation of why camera access is needed and ask for confirmation (Yes/No). This improves transparency, builds trust, and can reduce the likelihood of the user denying the request.
* **Handle permission rejection gracefully:**
  * **Soft rejection (user-level):** If the user declines the camera access within your app, provide a helpful message that explains the impact of this choice and give them the option to reconsider later.
  * **Hard rejection (system-level):** In case the user has previously denied the camera permission at the system level, guide them to the device settings where they can manually enable the camera access. Display an appropriate message explaining how to do this and the importance of enabling the permission for the app's functionality.

For Flutter applications, we recommend using the [permission\_handler](https://pub.dev/packages/permission_handler) package to manage camera permissions effectively.


# iOS 📱

Our `InsuranceCardScan` swift package makes it easy to add health insurance card scanning and eligibility verification to any iOS application in **5 minutes or less.**

### Installation

Find the [Add Package Dependency](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app) menu item in Xcode, File > Swift Packages.

Then enter the Github repository for the package:

```bash
https://github.com/CardScan-ai/insurance-card-scan-ios
```

### Usage

Import the package into your Xcode project files:

```swift
import InsuranceCardScan
```

### Basic Example:

Create the `CardScannerViewController` using the generated session token, present in the current view controller and register a callbacks to handle updates and errors.

```swift
import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var cardScanButton: UIButton!

    @IBAction private func didTapScanCard(_ sender: UIButton) {
        startCardScanning()
    }

    private func startCardScanning() {
        // Replace <GENERATED_USER_TOKEN> with the user token generated from the server
        // See https://docs.cardscan.ai/authentication#end-user

        let userToken = "<GENERATED_USER_TOKEN>"
        
        let onSuccessCallback: (InsuranceCard) -> Void = { card in
            print("Card Scanned Successfully! - \(card)")
        }

        let onErrorCallback: (CardScanError) -> Void = { error in
            print("Card Scanning Error: \(error.localizedDescription)")
        }
        
        // Configure and present the CardScanViewController
        let config = CardScanConfig(sessionToken: userToken, live: false, onSuccess: onSuccessCallback, onError: onErrorCallback)
        let cardScanViewController = CardScanViewController()
        cardScanViewController.config = config
        
        // Present the CardScanViewController
        present(cardScanViewController, animated: true)
    }
}

```

### Available Properties <a href="#available-properties" id="available-properties"></a>

`CardScanViewController` should be passed a `CardScanConfig` instance with properties for server connection, callback handling and UI customization.

```swift
CardScanConfig(
  // Required
  sessionToken: token,
  live: false,
  onSuccess: onSuccess,

  // Recommended
  onCancel: onCancel,
  onError: onError,

  //eligibility
  eligibility: eligibility,
  onEligibilitySuccess: onEligibilitySuccess,
  onEligibilityError: onEligibilityError,

  // Optional
  backsideSupport: scanBackside,
  onRetry: onRetry,
  onProgress: onProgress,
  
  // Camera Options
  cameraOptions: cameraOptions,

  // UI Customization
  messages: messages,
  messageStyle: messagesStyle,
  autoSwitchActiveColor: autoSwitchActiveColor,
  autoSwitchInactiveColor: autoSwitchInactiveColor,
  progressBarColor: progressBarColor,
  widgetBackgroundColor: widgetBackgroundColor,
  logging: logging,
)

```

### **Main** Properties

<table><thead><tr><th width="289">Prop</th><th width="112">Required</th><th width="135">Type</th><th>Description</th></tr></thead><tbody><tr><td>live</td><td>false</td><td>Boolean</td><td>Toggle the production or sandbox version. <strong>Default</strong>: false</td></tr><tr><td>sessionToken</td><td>true</td><td>String</td><td>A JWT token for the current user, see <a href="/pages/-MfnTIPuqKcHV1TvM04w">Authentication</a></td></tr><tr><td>onSuccess</td><td>true</td><td>Function</td><td>Called on successful scan. The first argument is the scanned card.</td></tr><tr><td>onCancel</td><td>false</td><td>Function</td><td>Triggered when the user cancels the Scanning UI.</td></tr><tr><td>onError</td><td>false</td><td>Function</td><td>Called when an error is returned by the API or the ViewController fails to initialize.</td></tr><tr><td>onProgress</td><td>true</td><td>Function</td><td>Progress updates during the card scanning process.</td></tr><tr><td>onRetry</td><td>false</td><td>Function</td><td>Called when a failed scan triggers a retry.</td></tr><tr><td>backsideSupport</td><td>false</td><td>Boolean</td><td>Enable scanning of the front and back side of the card.<br><strong>Default</strong>: false</td></tr><tr><td>eligibility</td><td>false</td><td>object</td><td>Request payload for the optional post-scan eligibility verification. See: <a data-mention href="/pages/F8HfWCgSF0hYgQuWMlRS">/pages/F8HfWCgSF0hYgQuWMlRS</a></td></tr><tr><td>onEligibilitySuccess</td><td>false</td><td>Function</td><td>Called on successful eligibility request. The eligibility response is pass as an argument.</td></tr><tr><td>onEligibilityError</td><td>false</td><td>Function</td><td>Called when an error is returned by the eligibility API.</td></tr><tr><td>cameraOptions</td><td>false</td><td>object</td><td>Configures the camera's orientation during scanning.</td></tr></tbody></table>

### UI/UX Customization Properties

The iOS widget is designed to be customizable. Please see the [#customization](#customization "mention") section of UI Components to adjust these elements to match your application's branding and theme:

[Customization ⚙️](/ui-components/customization)

{% hint style="info" %}
**Note:** All UI/UX Props are optional
{% endhint %}

<table><thead><tr><th width="329.3333333333333">Prop</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>messages</td><td>[String: String]</td><td>Customize the text displayed by the UI.</td></tr><tr><td>messageStyle</td><td>MessageStyle</td><td>Set the size, color and background color of the text displayed by the UI.</td></tr><tr><td>autoSwitchActiveColor</td><td>UIColor</td><td>Set the color of the auto scan switch</td></tr><tr><td>autoSwitchInactiveColor</td><td>UIColor</td><td>Set the color of the disabled auto scan switch</td></tr><tr><td>progressBarColor</td><td>UIColor</td><td>Set the color of the progress bars or bounding box that surrounds the card scanning area.</td></tr><tr><td>widgetBackgroundColor</td><td>UIColor</td><td>Set the main background color for the widget.</td></tr></tbody></table>

## Callbacks

### onSuccess Callback

The `onSuccess` callback is triggered when the card scanning process completes successfully. This function receives the scanned card data as an argument.

#### Usage

Define a function that receives the scanned card data and pass it to the `CardScanConfig`.

```swift
let handleCardScanSuccess: (InsuranceCard) -> Void = { card in
    print("Card scanned successfully: \(card)")
}

let config = CardScanConfig(
    sessionToken: "<GENERATED_USER_TOKEN>",
    onSuccess: handleCardScanSuccess
)

let cardScanViewController = CardScanViewController(config: config)
present(cardScanViewController, animated: true)

```

### onError Callback

The `onError` callback is executed when there is a failure during the card scanning process. This function receives an error object as an argument.

#### Usage

Define a function that receives an error object and pass it to the `CardScanConfig`.

```swift
let handleCardScanError: (CardScanError) -> Void = { error in
    print("Scanning failed: \(error.localizedDescription)")
}

let config = CardScanConfig(
    sessionToken: "<GENERATED_USER_TOKEN>",
    onError: handleCardScanError
)
```

In this example, the `handleCardScanError` function logs the error object to the console when a scanning failure occurs.

By using the `onSuccess` and `onError` props, you can handle successful and failed scanning events, allowing you to implement custom actions or display appropriate messages to the user.

{% hint style="info" %}
You can find examples of the[#error-screens](#error-screens "mention") at the bottom of this document
{% endhint %}

Possible errors returned to the `onError` callback.

```typescript
struct CardScanError: Error {
    let message: String
    let type: String
    let code: Int
}
```

<table><thead><tr><th width="235.66666666666666">Error Type</th><th>Error Code</th><th>Error Message</th></tr></thead><tbody><tr><td>VideoError</td><td>670</td><td>Various system errors including: “Permission Denied” and “Camera not found”</td></tr><tr><td>VideoError</td><td>675</td><td>Various system media errors, including: “Permission Denied” and “The element has no supported sources.”</td></tr><tr><td>VideoError</td><td>676</td><td>Any and all other system related video capture &#x26; canvas capture errors.</td></tr><tr><td>WSError</td><td>640</td><td>“No websocket found - critical failure”</td></tr><tr><td>WSError</td><td>642</td><td>“Unknown error from websocket”</td></tr><tr><td>WSError</td><td><a href="https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code">1002-1003, 1007-1015</a></td><td>“The connection was closed abnormally”</td></tr><tr><td>ResponseError</td><td>HTTP Codes</td><td>Various HTTP errors returned from XMLHttpRequest and the CardScan.ai backend.</td></tr><tr><td>RequestError</td><td>Various</td><td>Various HTTP errors returned from Axios and XMLHttpRequest.</td></tr><tr><td>Unknown</td><td>606</td><td>Possible Axios setup errors, websocket setup errors, etc.</td></tr></tbody></table>

### onCancel Callback

The `onCancel` callback enables you to execute a custom function when the user cancels the card scanning process. This can be useful for tracking user behavior, navigating to a different part of the application, or displaying an appropriate message.

#### Usage

Pass a function to the `CardScanConfig` that will be executed when the user cancels the scanning process.

```swift
let handleCancel: () -> Void = {
    print("Scanning cancelled by the user.")
}

let config = CardScanConfig(
    sessionToken: "<GENERATED_USER_TOKEN>",
    onCancel: handleCancel
)
```

### onRetry Callback

The `onRetry` callback allows you to execute a custom function when a retry is triggered due to a scanning failure.

#### Usage

Pass a function to the `CardScanConfig` that will be executed upon retry.

```swift
let handleRetry: () -> Void = {
    print("Retry triggered.")
}

let config = CardScanConfig(
    sessionToken: "<GENERATED_USER_TOKEN>",
    onRetry: handleRetry
)
```

In this example, the `handleRetry` function logs the retry event to the console when a retry is triggered.

### onProgress Callback

The `onProgress` callback allows you to execute a custom function to report progress during the scanning process.

#### Usage

```swift
let handleProgress: (ScanProgress) -> Void = { progress in
    print("Card ID: \(progress.cardId) - Scan Count: \(progress.scanCount) - Card State: \(progress.cardState)")
}

let config = CardScanConfig(
    sessionToken: "<GENERATED_USER_TOKEN>",
    onProgress: handleProgress
)
```

In this example, the `handleProgress` callback logs the progress during a live scanning operation. When `backsideSupport` is enabled the scan counter will reset to zero when the card is flipped to scan the back side.

{% hint style="info" %}
For the majority of live scanning scenarios, the **scanning will be completed within 2-3 scans.** In situations with low light or occluded card elements, the scan count can go as high as 12.
{% endhint %}

## Additional Features

### Camera Options

Camera Configuration allow developers to preset the camera's view orientation for specific installation scenarios, such as mirrored cameras or kiosks. These settings ensure the camera view is correctly aligned and oriented during the initial setup of the application:

* `flipHorizontal`: Enables horizontal flipping of camera view, reversing the left to right sides of the image (aka mirrored)
* `flipVertical`: Enabled vertical flipping of camera view, swapping the top and bottom of the image when the camera or document is upside down.

```swift
let config = CardScanConfig(
    sessionToken: "<GENERATED_USER_TOKEN>",
    onSuccess: onSuccessCallback,
    cameraOptions: CameraOptions(
        flipHorizontal: true,
        flipVertical: false
    )
)
```

### Eligibility Verification

Eligibility Verification is crucial in healthcare for confirming a patient's insurance coverage and benefits before services are provided, streamlining billing and enhancing patient care.

By providing subscriber and provider details through our React component, users can initiate the verification process effortlessly. The widget offers real-time feedback with success and error callbacks, simplifying integration into your application's UI.

See [Eligibility Verification 🩻](/advanced-features/eligibility-verification) for more details.

### Camera Permissions 📸

Our SDK requires camera access to scan insurance cards. While the CardScan widget automatically requests camera permissions during widget load, it does not present any UI for handling permissions or manage permission failures, particularly on mobile devices.

**Requirement:** We require that developers handle camera permission requests ***before*** loading the CardScan widget. This will allow you to manage the permission flow consistently within your application, providing custom error handling and user feedback if permission is denied.

**Recommendation:** We recommend following best practices when requesting camera permissions:

* **Pre-flight the request:**

  Within your app, prompt the user with a clear explanation of why camera access is needed and ask for confirmation (Yes/No). This improves transparency, builds trust, and can reduce the likelihood of the user denying the request.
* **Handle permission rejection gracefully:**
  * **Soft rejection (user-level):** If the user declines the camera access within your app, provide a helpful message that explains the impact of this choice and give them the option to reconsider later.
  * **Hard rejection (system-level):** In case the user has previously denied the camera permission at the system level, guide them to the device settings where they can manually enable the camera access. Display an appropriate message explaining how to do this and the importance of enabling the permission for the app's functionality.


# Android 🤖

Our `CardScanActivity` Kotlin Activity makes it easy to add insurance card scanning to any Android application in **5 minutes or less.**

### Requirements

* Android API level 23 or higher
* AndroidX compatibility
* Kotlin coroutine compatibility

### Installation

The library is published in the [maven central repository](https://search.maven.org/search?q=g:ai.cardscan), so installation is as simple as adding the dependency to your `build.gradle`

```
dependencies {
    implementation 'ai.cardscan:insurance-cardscan:0.3.5'   
}
```

### Usage

Import the necessary classes from the library into your activity or fragment:

```kotlin
import ai.cardscan.insurance.CardScanActivity
// import ai.cardscan.insurance.data.*
import ai.cardscan.insurance.data.CardScanHelper
import ai.cardscan.insurance.data.CardScanConfig
```

#### Basic usage Example:

```kotlin
import ai.cardscan.insurance.CardScanActivity
import ai.cardscan.insurance.data.CardScanConfig
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity

class OnboardingActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<View>(R.id.btnScanCard).setOnClickListener {
            setUpCardScanHelper()
            launchCardScanActivity()
        }
    }

    private fun launchCardScanActivity() {
        CardScanActivity.launch(
            // Pass an activity to the launcher
            this@MainActivity,
            // A CardScanConfig instance to connect to the server and customize UI
            CardScanConfig(
                // your token goes here
                sessionToken = "xxx",
            ),
            // And other optional parameters to alter behavior
            closeAfterSuccess = true, // Close the scanner after successful scan? 
                                    // defaults to true
            closeAfterSuccessDelay = 1500 // If set to close automatically, 
                                // how long to wait after scan to close? In milliseconds
            closeAfterErrorDelay = 2000 // Cardscan activity is killed automatically after an error
                                // You can decide between 0 and 30 seconds, how long to wait before closing                    
        )
    }
    
    
    private fun setUpCardScanHelper() {
        // Helper class to setup observers and create the appropiate callbacks
        // CardScanHelper takes in a lifecycleOwner, 
        // "this" in the case of setup within an activity,
        // or the host activity in the case of a fragment, or viewLifecycleOwner
        cardScanHelper = CardScanHelper(this).apply {
            onSuccess { card: ScannedCard ->
                Toast.makeText(this@MainActivity, "Card Scanned: ${card.cardId}", Toast.LENGTH_SHORT).show()
            }
            onError { error: CardError? ->
                Toast.makeText(this@MainActivity, "Error encountered!", Toast.LENGTH_SHORT).show()
                print(error?.message)
            }
            onRetry {
                Toast.makeText(this@MainActivity, "User retried after failed scan!", Toast.LENGTH_SHORT).show()
            }
            onCancel {
                Toast.makeText(this@MainActivity, "User closed the scanner with close button!", Toast.LENGTH_SHORT).show()
            }
            onEligibilitySuccess { eligibility:Eligibility ->
                Toast.makeText(this@MainActivity, "Eligibility info found!", Toast.LENGTH_SHORT).show()
            }
            onEligibilityError { eligibilityError:EligibilityError? ->
                Toast.makeText(this@MainActivity, "Error during eligibility check!", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

```

### Available Properties

When launching`CardScanActivity`, a `CardScanConfig` instance should be passed with properties for server connection and UI customization.

```kotlin
CardScanConfig(
  // Required
  sessionToken: token,
  live: false,

  // Optional
  backsideSupport: scanBackside,
  eligibility = EligibilityRequest( // We now support eligibility checks!
                    Subscriber(
                        firstName = "Subscriber's first name",
                        lastName = "Subscriber's last name",
                        dateOfBirth = "19020403" // Always in format YYYYMMDD
                    ),
                    Provider(
                        firstName = "Provider's first name",
                        lastName = "Provider's last name",
                        npi = "12345677"
                    )
                )

  // UI Customization
  messages = CardScanMessages(
                    autoCaptureTitle = "My Auto Capture Title",
                    manualCaptureTitle = "My Manual Capture Title",
                    processingTitle = "My Processing Title",
                    frontSideCompletedTitle = "My FrontSide Completed Title",
                    completedTitle = "My Completed Title",
                    retryTitle = "Me Retry Title",
                    errorTitle = "My Error Title",
                    cameraErrorTitle = "My Camera Error Title"
                )
  messageFontSize: messageFontSize, // Float
  messageTextColor: messageTextColor, // Int
  messageBackgroundColor: messageBackgroundColor, // Int
  autoSwitchActiveColor: autoSwitchActiveColor, // Int
  autoSwitchInactiveColor: autoSwitchInactiveColor, // Int
  progressBarColor: progressBarColor, // Int
  widgetBackgroundColor: widgetBackgroundColor, // Int
)
```

***

### Main Config Props

| Prop            | Required | Type               | Description                                                                                        |
| --------------- | -------- | ------------------ | -------------------------------------------------------------------------------------------------- |
| live            | false    | Boolean            | Toggle the production or sandbox version. **Default**: false                                       |
| sessionToken    | true     | String             | A JWT token for the current user, see [Authentication](/authentication)                            |
| backsideSupport | false    | Boolean            | <p>Enable scanning of the front and back side of the card.<br><strong>Default</strong>: false</p>  |
| eligibility     | false    | EligibilityRequest | Send an EligibilityRequest object with the properties shown above to perform an eligibility check. |

### UI/UX Customization Props

The card scanner view is designed to be customizable. Please see the [Customization ⚙️](/ui-components/customization) section of UI Components to adjust these elements to match your application's branding and theme:

{% hint style="info" %}
**Note:** All UI/UX Props are optional
{% endhint %}

| Prop                    | Type   | Description                                                                               |
| ----------------------- | ------ | ----------------------------------------------------------------------------------------- |
| messages                | Object | Customize the text displayed by the UI.                                                   |
| messageFontSize         | Float  | Set the size of the text displayed by the UI.                                             |
| messageTextColor        | Int    | Set the color of the text displayed by the UI.                                            |
| messageBackgroundColor  | Int    | Set the background color of the text displayed by the UI.                                 |
| autoSwitchActiveColor   | Int    | Set the color of the auto scan switch.                                                    |
| autoSwitchInactiveColor | Int    | Set the color of the disabled auto scan switch.                                           |
| progressBarColor        | Int    | Set the color of the progress bars or bounding box that surrounds the card scanning area. |
| widgetBackgroundColor   | Int    | Set the main background color for the widget.                                             |

### Cardscan activity launcher props

| Prop                   | Type           | Description                                                                                 |
| ---------------------- | -------------- | ------------------------------------------------------------------------------------------- |
| activity               | Activity       | The activity from which the CardScan activity will be started. Usually MainActivity         |
| properties             | CardScanConfig | Object to set the parameters for the scanner                                                |
| closeAfterSuccess      | Boolean        | Whether to automatically finish Cardscan activity after successful scan.                    |
| closeAfterSuccessDelay | Long           | Time in milliseconds to wait before finishing Cardscan activity, after a successful scan.   |
| closeAfterErrorDelay   | Long           | Time in milliseconds to wait before finishing Cardscan activity, after an error was thrown. |

### onSuccess Callback

`onSuccess` triggers when card scanning process completes successfully. This function receives the scanned card data as an argument.

#### Usage

To use the `onSuccess` callback, pass a function that receives the scanned card data to the onSuccess function in CardScanHelper:

```kotlin
fun handleCardScanSuccess(card: ScannedCard?) {
  print("Card scanned successfully: $card")
}

 private fun setUpCardScanHelper() {
        cardScanHelper = CardScanHelper(this).apply {
            onSuccess { card: ScannedCard ->
                handleCardScanSuccess(card)
            }
        }
    }
)
```

In this example, the `handleCardScanSuccess` function logs the scanned card data to the console when the scanning process is completed successfully and your MainActivity or fragment resumes.

### onError Callback

`onError` triggers after a failure occurs during the card scanning process. This function receives an error object as an argument.

#### Usage

Pass a function that receives the error object or implement your callback directly inside the helper:

```kotlin
fun handleCardScanError(card: CardError?) {
  print("Scanning failed: $card")
}

 private fun setUpCardScanHelper() {
        cardScanHelper = CardScanHelper(this).apply {
            onSuccess { card: ScannedCard ->
                handleCardScanSuccess(card)
            }
            onError { error: CardError? -> 
                handleCardScanError(error)
            }
        }
    }
)
```

In this example, the `handleCardScanError` logs the error object when a scanning failure occurs.

By using the `onSuccess` and `onError` callbacks, you can handle successful and failed scanning event with custom actions or display appropriate messages to the user.

### onCancel and onRetry Callbacks

`onCancel` expects a function that will trigger after users cancel the card scanning process. This could be useful for tracking user behavior or displaying an appropriate message. `onRetry` expects a function that will trigger when your activity or fragment resumes, if the user had a retry of a scanning process.

```kotlin
private fun setUpCardScanHelper() {
        cardScanHelper = CardScanHelper(this).apply {
            onSuccess { card: ScannedCard ->
                handleCardScanSuccess(card)
            }
            onError { error: CardError? -> 
                handleCardScanError(error)
            }
            onCancel {
                // Handle a user closing the scanner with the close button!
            }
            onRetry {
                // Triggered after the user retried a scan
            }
        }
    }
```

### onEligibilitySuccess and onEligibilityError Callbacks

Both callbacks expect a function that will trigger in either case. `onElibiligitySuccess` callback will pass an `Eligibility` object and `onEligibilityError` will pas an `EligibilityError` object. Expect the same behavior as the other callbacks, this objects will be passed once your activity or fragment resumes.

```kotlin
private fun setUpCardScanHelper() {
        cardScanHelper = CardScanHelper(this).apply {
            onSuccess { card: ScannedCard ->
                handleCardScanSuccess(card)
            }
            onError { error: CardError? -> 
                handleCardScanError(error)
            }
            onCancel {
                // Handle a user closing the scanner with the close button!
            }
            onRetry {
                // Triggered after the user retried a scan
            }
            onEligibilitySuccess { eligibility:Eligibility ->
                handleEligibilitySuccess(eligibility)
            }
            onEligibilityError { eligibilityError:EligibilityError? ->
                handleEligibilityError(eligibilityError)
            }
        }
    }
```

### Camera Permissions 📸

Our SDK requires camera access to scan insurance cards. While the CardScan widget automatically requests camera permissions during widget load, it does not present any UI for handling permissions or manage permission failures, particularly on mobile devices.

**Requirement:** We require that developers handle camera permission requests ***before*** loading the CardScan widget. This will allow you to manage the permission flow consistently within your application, providing custom error handling and user feedback if permission is denied.

**Recommendation:** We recommend following best practices when requesting camera permissions:

* **Pre-flight the request:**

  Within your app, prompt the user with a clear explanation of why camera access is needed and ask for confirmation (Yes/No). This improves transparency, builds trust, and can reduce the likelihood of the user denying the request.
* **Handle permission rejection gracefully:**

  * **Soft rejection (user-level):** If the user declines the camera access within your app, provide a helpful message that explains the impact of this choice and give them the option to reconsider later.
  * **Hard rejection (system-level):** In case the user has previously denied the camera permission at the system level, guide them to the device settings where they can manually enable the camera access. Display an appropriate message explaining how to do this and the importance of enabling the permission for the app's functionality.

  For Android applications, we recommend taking a dive into the official docs: <https://developer.android.com/training/permissions/requesting>

#### Suggestion for code to include with Cardscan launcher:

```kotlin
// Permission launcher, displays dialog asking for permission
val requestPermissionLauncher =
    registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            // Permission is granted, you can launch Cardscan activity safely
            launchCardScanActivity()
        } else {
            // Permission denied or dismissed.
            // Explain to user that feature is unavailable, because permission
            // is needed to use it.
        }
    }   
```

```kotlin
// Check permission status and act on it 
     private fun handleScannerOpen() {
        when {
            ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.CAMERA
            ) == PackageManager.PERMISSION_GRANTED -> {
                // Permission is granted, you can start
                // Cardscan activity safely.
                launchCardScanActivity()
            }

            ActivityCompat.shouldShowRequestPermissionRationale(
                this,
                Manifest.permission.CAMERA
            ) -> {
                // Show UI to app user to tell them why you need the permission.
                // This is a good place to take them to settings too
                // so they can grant permission there.
            }

            else -> {
                // Here you can directly ask for the permission
                requestPermissionLauncher.launch(Manifest.permission.CAMERA)
            }
        }
    }
```


# Customization ⚙️

## Message Customization

All of the UI widgets are designed to provide helpful messages and status updates to users during the scanning process. To enhance user experience, you can customize these messages to display application-specific text instead of the default message.

#### Default Message Example Image

<div align="center"><figure><img src="/files/tFMZZDcO6TM6V8eFfADc" alt=""><figcaption></figcaption></figure></div>

This example image shows the default message displayed on the screen during the card scanning process. You can customize these messages using the available options.

### Available Customization Options

<table><thead><tr><th width="184.33333333333331">State</th><th>Key</th><th>Default Message</th></tr></thead><tbody><tr><td><strong>Auto Start</strong></td><td>autoCaptureTitle</td><td><p>Hold Card in Frame</p><p>It Will Scan Automatically</p></td></tr><tr><td><strong>Manual</strong> <strong>Start</strong></td><td>manualCaptureTitle</td><td>Use camera button to scan card</td></tr><tr><td><strong>Processing</strong></td><td>processingTitle</td><td>Keep Card Still While Scanning</td></tr><tr><td><strong>Manual Processing</strong></td><td>manualProcessingTitle</td><td>Reading card details, one moment...</td></tr><tr><td><strong>Frontside Completed</strong></td><td>frontsideCompletedTitle</td><td><p>Front side scan is complete</p><p>Rotate card to scan back</p></td></tr><tr><td><strong>Completed</strong></td><td>completedTitle</td><td>Card Scanned Successfully</td></tr><tr><td><strong>Retry</strong></td><td>retryTitle</td><td>Scanning Failed - Please Try Again</td></tr><tr><td><strong>Error</strong></td><td>errorTitle</td><td>Setup Failure - Please Reload the Page</td></tr><tr><td><strong>CameraError</strong></td><td>cameraErrorTitle</td><td>Unable to Setup Camera for Capture</td></tr><tr><td><strong>Eligibility Processing</strong></td><td>eligibilityProcessingTitle</td><td>Checking eligibility</td></tr><tr><td><strong>Post Processing</strong></td><td>postProcessingTitle</td><td>Validating Insurance Card...</td></tr><tr><td><strong>Eligibility Error</strong></td><td>eligibilityErrorTitle</td><td>Eligibility Setup Failure</td></tr></tbody></table>

#### Example

Here's an example of how to customize the default message displayed when initiating the scanning process:

{% tabs %}
{% tab title="React" %}

```jsx
const messages = { 
    autoCaptureTitle: "Place card on a well-lit, non-reflective surface to start scanning", 
}

return (
    <CardScanView 
        sessionToken={token}
        onSuccess={cardScanSuccess}
        onCancel={cardScanCancel}
        messages={messages}
    />
);
```

{% endtab %}

{% tab title="React Native" %}

```jsx
const messages = { 
    autoCaptureTitle: "Place card on a well-lit, non-reflective surface to start scanning", 
}

return (
    <CardScanView 
        sessionToken={token}
        onSuccess={cardScanSuccess}
        onCancel={cardScanCancel}
        messages={messages}
    />
);
```

{% endtab %}

{% tab title="Flutter" %}
{% code overflow="wrap" %}

```dart
const messages = CardScanMessages(
  autoCaptureTitle: 'Place card on a well-lit, non-reflective surface to start scanning',
);

CardScanner(
  properties: CardScanConfig(
    sessionToken: sessionToken,
    onSuccess: cardScanSuccess,
    messages: messages,
  ),
);
```

{% endcode %}
{% endtab %}

{% tab title="iOS" %}

```swift
let messages = CardScanMessages(
    autoCaptureTitle: "Place card on a well-lit, non-reflective surface to start scanning"
)

let config = CardScanConfig(
    sessionToken: token,
    onSuccess: onSuccess,
    messages: messages
)

let cardScanViewController = CardScanViewController()
cardScanViewController.config = config
```

{% endtab %}

{% tab title="Android" %}

```kotlin
val messages = CardScanMessages(
    autoCaptureTitle = "Place card on a well-lit, non-reflective surface to start scanning"
)

CardScanConfig(
    sessionToken = token,
    onSuccess = { card -> /* handle success */ },
    messages = messages
)
```

{% endtab %}
{% endtabs %}

Replace the text within the quotes to suit your application's requirements.

<figure><img src="/files/C3bbgayhuym718OvToqC" alt=""><figcaption></figcaption></figure>

This example image shows the scanning process in action, with the customized `autoCaptureTitle` message displayed on the screen.

### Scanning Process

The chart below illustrates the flow of messages displayed to the user during the card scanning process:

<figure><img src="/files/dIPnqcaELBTFxpShTCJE" alt=""><figcaption><p>Scanning Process</p></figcaption></figure>

The diagram shows how the user is guided through the scanning process, starting with the initial instructions for holding the card in the frame, progressing through the processing states, and ultimately either succeeding or requiring a retry or displaying an error.

### Text Size, Text Color, and Background Color

To customize the text size, text color, and background color of the content, you can pass the respective text style and color properties to the CardScanView widget:

{% tabs %}
{% tab title="React" %}

```jsx
<CardScanView 
  sessionToken={token}
  messageFontSize="18px"
  messageTextColor="blue"
  messageBackgroundColor="lightgray"
/>
```

{% endtab %}

{% tab title="React - CSS" %}

```css
/* React Web SDK supports CSS custom properties for message styling */
.cardscan-widget {
  --message-font-size: 18px;
  --message-text-color: blue;
  --message-background-color: lightgray;
  --message-font-family: 'Arial', sans-serif;
  --message-font-weight: 600;
}
```

{% endtab %}

{% tab title="React Native" %}

```jsx
<CardScanView 
  sessionToken={token}
  messageStyle={{
    fontSize: 18,
    color: "blue",
    backgroundColor: "lightgray"
  }}
/>
```

{% endtab %}

{% tab title="Flutter" %}

```dart
CardScanner(
  properties: CardScanConfig(
    sessionToken: sessionToken,
    onSuccess: cardScanSuccess,
    messageStyle: TextStyle(
      fontSize: 18.0,
      color: Colors.blue,
      backgroundColor: Colors.grey,
    ),
    messages: messages,
  ),
);
```

{% endtab %}

{% tab title="iOS" %}

```swift
let messageStyle = MessageStyle(
    fontSize: 18.0,
    textColor: UIColor.blue,
    backgroundColor: UIColor.lightGray
)

let config = CardScanConfig(
    sessionToken: token,
    onSuccess: onSuccess,
    messageStyle: messageStyle
)

let cardScanViewController = CardScanViewController()
cardScanViewController.config = config
```

{% endtab %}

{% tab title="Android" %}

```kotlin
CardScanConfig(
    sessionToken = token,
    messageFontSize = 18.0f,
    messageTextColor = Color.BLUE,
    messageBackgroundColor = Color.LTGRAY
)
```

{% endtab %}
{% endtabs %}

This example changes the text size to 18px, the text color to blue, and the background color to gray.

## UX Customization

This documentation section provides guidelines on how to customize the user experience of the card scanning component, focusing on visual aspects such as the auto/manual toggle switch color, progress bar color, and widget background color. The examples below demonstrate how to adjust these elements to match your application's branding or theme.

### Auto/Manual Toggle Switch Color

The auto/manual toggle switch allows users to switch between automatic and manual scanning modes. You can change its colors to match your application's design.

![](/files/O9C9zQrHlrFAknhYj6ku) ![](/files/9wQZnJF6mdcafwuIfCSW)

To change the colors of the auto/manual toggle switch, use the following props:

* `autoSwitchActiveColor` - sets the color of the active switch
* `autoSwitchInactiveColor` - sets the color of the inactive switch

Here's an example of how to set the active switch color to blue and the inactive switch color to grey:

{% tabs %}
{% tab title="React" %}

```jsx
<CardScanView 
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onCancel={cardScanCancel}
  autoSwitchActiveColor="blue"
  autoSwitchInactiveColor="grey"
/>
```

{% endtab %}

{% tab title="React - CSS" %}

```css
/* React Web SDK supports CSS customization */
.cardscan-widget {
  --auto-switch-active-color: blue;
  --auto-switch-inactive-color: grey;
}
```

{% endtab %}

{% tab title="React Native" %}

```jsx
<CardScanView 
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onCancel={cardScanCancel}
  autoSwitchActiveColor="blue"
  autoSwitchInactiveColor="grey"
/>
```

{% endtab %}

{% tab title="Flutter" %}

```dart
CardScanner(
  properties: CardScanConfig(
    sessionToken: sessionToken,
    onSuccess: cardScanSuccess,
    autoSwitchActiveColor: Colors.blue,
    autoSwitchInactiveColor: Colors.grey,
  ),
);
```

{% endtab %}

{% tab title="iOS" %}

```swift
let config = CardScanConfig(
    sessionToken: token,
    onSuccess: onSuccess,
    autoSwitchActiveColor: UIColor.blue,
    autoSwitchInactiveColor: UIColor.lightGray
)
```

{% endtab %}

{% tab title="Android" %}

```kotlin
CardScanConfig(
    sessionToken = token,
    autoSwitchActiveColor = Color.BLUE,
    autoSwitchInactiveColor = Color.LTGRAY
)
```

{% endtab %}
{% endtabs %}

### Progress Bar Color

The progress bar provides visual feedback to users during the scanning process, indicating the current progress and scanning state (automatic vs manual).

<figure><img src="/files/DZHMXAcq8Pveu0rwPtyC" alt=""><figcaption></figcaption></figure>

To change the color of the progress bar, use the `progressBarColor` prop:

{% tabs %}
{% tab title="React" %}

```jsx
<CardScanView 
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onCancel={cardScanCancel}
  progressBarColor="red"
/>
```

{% endtab %}

{% tab title="React - CSS" %}

```css
/* React Web SDK supports CSS customization */
.cardscan-widget {
  --progress-bar-color: red;
}
```

{% endtab %}

{% tab title="React Native" %}

```jsx
<CardScanView 
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onCancel={cardScanCancel}
  progressBarColor="red"
/>
```

{% endtab %}

{% tab title="Flutter" %}

```dart
CardScanner(
  properties: CardScanConfig(
    sessionToken: sessionToken,
    onSuccess: cardScanSuccess,
    progressBarColor: Colors.red,
  ),
);
```

{% endtab %}

{% tab title="iOS" %}

```swift
let config = CardScanConfig(
    sessionToken: token,
    onSuccess: onSuccess,
    progressBarColor: UIColor.red
)
```

{% endtab %}

{% tab title="Android" %}

```kotlin
CardScanConfig(
    sessionToken = token,
    progressBarColor = Color.RED
)
```

{% endtab %}
{% endtabs %}

In this example, the progress bar color is set to red, replace `red` with the desired color.

### Widget Background Color

The widget background color can be customized to seamlessly integrate the component with your application's design.

<figure><img src="/files/f5M2lD9iGiLUxm2x8zkN" alt=""><figcaption></figcaption></figure>

\
To change the background color of the UI widget, use the `widgetBackgroundColor` prop:

{% tabs %}
{% tab title="React" %}

```jsx
<CardScanView 
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onCancel={cardScanCancel}
  widgetBackgroundColor="#f0f0f0"
/>
```

{% endtab %}

{% tab title="React - CSS" %}

```css
/* React Web SDK supports CSS customization */
.cardscan-widget {
  --widget-background-color: #f0f0f0;
}
```

{% endtab %}

{% tab title="React Native" %}

```jsx
<CardScanView 
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onCancel={cardScanCancel}
  widgetBackgroundColor="#f0f0f0"
/>
```

{% endtab %}

{% tab title="Flutter" %}

```dart
CardScanner(
  properties: CardScanConfig(
    sessionToken: sessionToken,
    onSuccess: cardScanSuccess,
    widgetBackgroundColor: Color(0xFFF0F0F0),
  ),
);
```

{% endtab %}

{% tab title="iOS" %}

```swift
let config = CardScanConfig(
    sessionToken: token,
    onSuccess: onSuccess,
    widgetBackgroundColor: UIColor(hex: "#f0f0f0")
)
```

{% endtab %}

{% tab title="Android" %}

```kotlin
CardScanConfig(
    sessionToken = token,
    widgetBackgroundColor = Color.parseColor("#f0f0f0")
)
```

{% endtab %}
{% endtabs %}

Replace `Color(0xFFF0F0F0)` with the desired background color.

By customizing these visual aspects, you can provide a consistent user experience that aligns with your application's overall design and branding.

### Success Indicator

After successful scanning and processing of the insurance card, the UI widget will display a checkmark icon in the center of the bounding box.

The indicator can be replaced by passing a `ReactNode` into the `successIndicator` prop.

**Note:** Currently only supported by the React UI Widget

<figure><img src="/files/tx5HRqXySVg22scInNjg" alt=""><figcaption><p>Successful Scan UI</p></figcaption></figure>

Here's an example of how to create a custom success indicator using an SVG checkmark icon and a message:

```jsx
import { ReactComponent as CheckmarkIcon } from './checkmark-icon.svg';

<CardScanView
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onCancel={cardScanCancel}
  successIndicator={<CheckmarkIcon />}
/>
```

### Error Indicator

If a system error occurs during card processing the react widget will display an error icon in the center of the bounding box.

The indicator can be replaced by passing a `ReactNode` into the `errorIndicator` prop.

**Note:** Currently only supported by the React UI Widget

![](/files/7Hs8avdCinnfhGBmVkFq)

Here's an example of how to create a custom error indicator using an SVG error icon and a message:

```jsx
import { ReactComponent as ErrorIcon } from './error-icon.svg';

<CardScanView
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onCancel={cardScanCancel}
  content={customizedContent}
  errorIndicator={<ErrorIcon />}
/>

```

### Close Button

The close button is displayed in the top right-hand corner of the react widget. When touched or clicked it will stop the camera stream, clean up the component, and then call the `onCancel` handler.

**Note:** Currently only supported by the React UI Widget

![](/files/efN67yC3FkCz9MYSYymX)

The close button can be replaced by passing a `ReactNode` into the `closeButton` prop.

```jsx
const customCloseButton = () => {
    return (
      <button type="button"  className="btn btn-secondary">Close</button>
    )
  };
  
<CardScanView 
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onCancel={cardScanCancel}
  closeButton={customCloseButton()}
/>
```

## React Web SDK - Complete CSS Customization

The React web SDK (`@cardscan.ai/insurance-cardscan-react`) supports comprehensive CSS customization through CSS custom properties (CSS variables). You can override any of these variables to match your application's design system.

### Complete CSS Variables Reference

```css
/* Main widget container */
.cardscan-widget {
  /* Message styling */
  --message-font-size: 16px;
  --message-text-color: #333333;
  --message-background-color: rgba(255, 255, 255, 0.9);
  --message-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --message-font-weight: 500;
  --message-border-radius: 8px;
  --message-padding: 12px 16px;
  
  /* Auto/Manual toggle switch */
  --auto-switch-active-color: #007bff;
  --auto-switch-inactive-color: #6c757d;
  --auto-switch-background-color: #ffffff;
  --auto-switch-border-color: #dee2e6;
  --auto-switch-border-radius: 20px;
  
  /* Progress bar */
  --progress-bar-color: #28a745;
  --progress-bar-background-color: #e9ecef;
  --progress-bar-height: 4px;
  --progress-bar-border-radius: 2px;
  
  /* Widget background and layout */
  --widget-background-color: #000000;
  --widget-border-radius: 12px;
  --widget-padding: 20px;
  --widget-min-height: 400px;
  
  /* Camera preview */
  --camera-preview-border-color: #ffffff;
  --camera-preview-border-width: 2px;
  --camera-preview-border-radius: 8px;
  
  /* Buttons */
  --button-primary-background: #007bff;
  --button-primary-color: #ffffff;
  --button-primary-border-radius: 6px;
  --button-primary-padding: 10px 20px;
  --button-primary-font-weight: 600;
  
  --button-secondary-background: #6c757d;
  --button-secondary-color: #ffffff;
  --button-secondary-border-radius: 6px;
  
  /* Success/Error indicators */
  --success-indicator-color: #28a745;
  --success-indicator-size: 48px;
  --error-indicator-color: #dc3545;
  --error-indicator-size: 48px;
  
  /* Loading spinner */
  --loading-spinner-color: #007bff;
  --loading-spinner-size: 32px;
  
  /* Close button */
  --close-button-color: #ffffff;
  --close-button-background: rgba(0, 0, 0, 0.5);
  --close-button-size: 32px;
  --close-button-border-radius: 50%;
}
```

### Usage Examples

#### Matching Your Brand Colors

```css
.cardscan-widget {
  --auto-switch-active-color: #ff6b35;  /* Your brand primary */
  --progress-bar-color: #ff6b35;
  --button-primary-background: #ff6b35;
  --success-indicator-color: #00c851;   /* Your success color */
}
```

#### Dark Theme Support

```css
.cardscan-widget.dark-theme {
  --widget-background-color: #1a1a1a;
  --message-text-color: #ffffff;
  --message-background-color: rgba(0, 0, 0, 0.7);
  --camera-preview-border-color: #666666;
  --auto-switch-active-color: #4dabf7;
}
```

#### Minimal/Clean Style

```css
.cardscan-widget.minimal {
  --widget-padding: 10px;
  --widget-border-radius: 0;
  --message-background-color: transparent;
  --camera-preview-border-width: 1px;
  --button-primary-border-radius: 0;
}
```

{% hint style="info" %}
**Are we missing something?** Please let us know and we would be happy to add it. Contact [support](mailto:support@cardscan.ai).
{% endhint %}


# Overview 📦

CardScan provides pre-built API clients for multiple programming languages to simplify integration with our platform. These clients are generated from our [OpenAPI specification](https://github.com/CardScan-ai/api-clients/blob/main/openapi.yaml) and provide type-safe interfaces for all API operations.

## Available Clients

| Language              | Package                        | Repository                                                        |
| --------------------- | ------------------------------ | ----------------------------------------------------------------- |
| TypeScript/JavaScript | `@cardscan.ai/cardscan-client` | [npm](https://www.npmjs.com/package/@cardscan.ai/cardscan-client) |
| Python                | `cardscan-client`              | [PyPI](https://pypi.org/project/cardscan-client/)                 |
| Swift                 | `CardScanClient`               | [Swift Package](https://github.com/CardScan-ai/api-clients)       |
| Kotlin                | `com.cardscan.api`             | [Maven](https://search.maven.org/artifact/com.cardscan/api)       |
| Dart                  | `cardscan_client`              | [pub.dev](https://pub.dev/packages/cardscan_client)               |

## Installation

### TypeScript/JavaScript

```bash
npm install @cardscan.ai/cardscan-client
# or
yarn add @cardscan.ai/cardscan-client
```

### Python

```bash
pip install cardscan-client
```

### Swift

```swift
// Package.swift
dependencies: [
    .package(url: "https://github.com/CardScan-ai/api-clients.git", from: "1.0.0")
]
```

### Kotlin

```kotlin
// build.gradle
implementation 'com.cardscan:api:1.0.0'
```

### Dart

```yaml
# pubspec.yaml
dependencies:
  cardscan_client: ^1.0.0
```

## Quick Start

All clients follow a similar pattern:

1. Initialize the client with your API key
2. Generate a session token for your user
3. Use the client to create cards, upload images, and retrieve results

```typescript
// TypeScript example
import { CardScanApi } from '@cardscan.ai/cardscan-client';

const client = new CardScanApi({ 
  apiKey: 'sk_test_cardscan_ai_...' 
});

// Generate session token
const { Token, IdentityId, session_id } = await client.getAccessToken({ 
  user_id: 'unique-user-id' 
});

// Create a card
const card = await client.createCard({
  sessionToken: Token,
  enable_backside_scan: false
});
```

## Features

All API clients provide:

* **Type Safety**: Strongly typed request and response objects
* **Authentication**: Built-in handling of API keys and session tokens
* **Error Handling**: Consistent error types and messages
* **Async Support**: Modern async/await patterns (where applicable)
* **Auto-retry**: Configurable retry logic for transient failures
* **Documentation**: Inline documentation and code completion

## Source Code

The API clients are open source and available at: <https://github.com/CardScan-ai/api-clients>

## Custom Clients

If you need to generate a client for a language not listed above, you can use our OpenAPI specification with any OpenAPI code generator:

```bash
# Example using OpenAPI Generator
openapi-generator generate \
  -i https://raw.githubusercontent.com/CardScan-ai/api-clients/main/openapi.yaml \
  -g <language> \
  -o ./generated-client
```


# TypeScript/JavaScript 📜

The official TypeScript/JavaScript client for the CardScan API provides a type-safe interface for all API operations.

## Installation

```bash
npm install @cardscan.ai/cardscan-client
# or
yarn add @cardscan.ai/cardscan-client
# or
pnpm add @cardscan.ai/cardscan-client
```

## Basic Usage

```typescript
import { CardScanApi } from '@cardscan.ai/cardscan-client';

// Initialize with your API key
const apiKey = 'sk_test_cardscan_ai_...';
const client = new CardScanApi({ apiKey });

// Generate a session token for a user
const { Token, IdentityId, session_id } = await client.getAccessToken({
  user_id: 'unique-user-123'
});

// Initialize client with session token for frontend operations
const userClient = new CardScanApi({ 
  sessionToken: Token,
  live: false // Use sandbox environment
});
```

## Quick Start with fullScan

The easiest way to scan cards is using the `fullScan` helper method that handles the entire workflow:

```typescript
// Initialize with websocket URL for fullScan support
const client = new CardScanApi({
  apiKey: 'sk_test_cardscan_ai_...',
  websocketUrl: 'wss://sandbox.cardscan.ai/v1/ws' // Required for fullScan
});

// Scan a card (front and back)
async function scanCard() {
  // For Node.js - using file paths
  const result = await client.fullScan({
    frontImage: './front-card.jpg',
    backImage: './back-card.jpg'  // Optional - omit for front-only
  });
  
  // For browser - using File objects from input
  const frontFile = document.getElementById('front-input').files[0];
  const backFile = document.getElementById('back-input').files[0];
  
  const result = await client.fullScan({
    frontImage: frontFile,
    backImage: backFile  // Optional
  });
  
  // Access the results
  console.log('Card ID:', result.card_id);
  console.log('Member ID:', result.details.member_id);
  console.log('Group:', result.details.group_number);
  console.log('Payer:', result.details.payer_name);
  
  return result;
}
```

The `fullScan` method automatically:

* Creates a card with appropriate settings
* Generates upload URLs
* Uploads images in the correct order
* Monitors processing via WebSocket
* Returns the completed card with all extracted data

## Manual Card Scanning Workflow

For more control over the scanning process, you can use the step-by-step approach:

### 1. Create a Card

```typescript
const card = await userClient.createCard({
  enable_backside_scan: false,
  enable_livescan: false,
  metadata: {
    patient_id: '12345',
    visit_id: 'v-67890'
  }
});

console.log('Card ID:', card.card_id);
console.log('State:', card.state); // 'pending'
```

### 2. Generate Upload URL

```typescript
const uploadData = await userClient.generateCardUploadUrl({
  card_id: card.card_id,
  orientation: 'front',
  capture_type: 'manual'
});

// uploadData contains:
// - upload_url: Pre-signed S3 URL
// - upload_parameters: Form data fields for upload
```

### 3. Upload Image

```typescript
// Create form data with upload parameters
const formData = new FormData();
Object.entries(uploadData.upload_parameters).forEach(([key, value]) => {
  formData.append(key, value as string);
});

// Add the image file last
formData.append('file', imageFile);

// Upload directly to S3
await fetch(uploadData.upload_url, {
  method: 'POST',
  body: formData
});
```

### 4. Poll for Results

```typescript
async function waitForCompletion(cardId: string): Promise<CardApiResponse> {
  while (true) {
    const card = await userClient.getCard({ card_id: cardId });
    
    if (card.state === 'completed' || card.state === 'error') {
      return card;
    }
    
    // Wait 2 seconds before polling again
    await new Promise(resolve => setTimeout(resolve, 2000));
  }
}

const completedCard = await waitForCompletion(card.card_id);
console.log('Insurance Info:', completedCard.details);
```

## Error Handling

```typescript
import { ApiError } from '@cardscan.ai/cardscan-client';

try {
  const card = await client.getCard({ card_id: 'invalid-id' });
} catch (error) {
  if (error instanceof ApiError) {
    console.error('API Error:', error.status, error.body);
    
    switch (error.status) {
      case 401:
        console.error('Invalid API key or session token');
        break;
      case 404:
        console.error('Card not found');
        break;
      case 429:
        console.error('Rate limit exceeded');
        break;
    }
  }
}
```

## WebSocket Support

For real-time updates, connect to the WebSocket API:

```typescript
const ws = new WebSocket(`wss://sandbox.cardscan.ai/v1/ws?token=${Token}`);

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  
  switch (message.type) {
    case 'card.processing':
      console.log('Card processing started');
      break;
    case 'card.completed':
      console.log('Card completed:', message.card_id);
      break;
    case 'card.error':
      console.error('Card error:', message.error);
      break;
  }
};
```

## Eligibility Verification

```typescript
// Create eligibility request
const eligibility = await client.createEligibility({
  card_id: completedCard.card_id,
  eligibility: {
    provider: {
      first_name: 'John',
      last_name: 'Smith',
      npi: '1234567890'
    },
    subscriber: {
      first_name: completedCard.details.member_name,
      last_name: completedCard.details.member_name,
      date_of_birth: '1980-01-01'
    }
  }
});

// Poll for eligibility results
const eligibilityResult = await waitForEligibilityCompletion(
  eligibility.eligibility_id
);
```

## Helper Methods

### Full Scan Helper

The client includes a `fullScan` helper method that simplifies the entire card scanning workflow:

```typescript
import { CardScanApi } from '@cardscan.ai/cardscan-client';
import { createReadStream } from 'fs'; // Node.js only

const client = new CardScanApi({
  apiKey: 'sk_test_cardscan_ai_...',
  websocketUrl: 'wss://sandbox.cardscan.ai' // Required for fullScan
});

// Full card scan with front and back images
async function scanCard() {
  try {
    // Node.js - using file streams
    const frontImage = createReadStream('./front-card.jpg');
    const backImage = createReadStream('./back-card.jpg');
    
    // Browser - using File objects from input[type="file"]
    // const frontImage = document.getElementById('front-input').files[0];
    // const backImage = document.getElementById('back-input').files[0];
    
    // The fullScan method handles:
    // 1. Creating a card with appropriate settings
    // 2. Uploading images in correct order (front first, then back)
    // 3. Waiting for processing completion via websockets
    // 4. Returning the final card data with extracted information
    const cardResult = await client.fullScan({
      frontImage: frontImage,
      backImage: backImage, // Optional - omit for front-only scanning
    });
    
    console.log('Card ID:', cardResult.card_id);
    console.log('Extracted Data:', cardResult.details);
    
    // Access extracted information
    if (cardResult.details) {
      console.log('Member ID:', cardResult.details.member_id);
      console.log('Plan Name:', cardResult.details.plan_name);
      console.log('Payer Name:', cardResult.details.payer_name);
    }
  } catch (error) {
    console.error('Card scan failed:', error);
  }
}

// Front-only scan
async function scanFrontOnly() {
  const frontImage = createReadStream('./front-card.jpg');
  
  const cardResult = await client.fullScan({
    frontImage: frontImage
    // No backImage parameter for front-only scanning
  });
  
  console.log('Front-only scan completed:', cardResult);
}
```

## TypeScript Types

The client includes full TypeScript definitions for all API operations:

```typescript
import type { 
  CardApiResponse,
  CardState,
  CreateCardRequest,
  EligibilityApiResponse,
  WebhookEvent 
} from '@cardscan.ai/cardscan-client';

// All responses are fully typed
function processCard(card: CardApiResponse): void {
  if (card.state === 'completed' && card.details) {
    console.log('Member ID:', card.details.member_id);
    console.log('Group Number:', card.details.group_number);
    
    // TypeScript knows these fields might be undefined
    if (card.details.rx_bin) {
      console.log('RX BIN:', card.details.rx_bin);
    }
  }
}
```

## Configuration Options

```typescript
const client = new CardScanApi({
  apiKey: 'sk_test_cardscan_ai_...',
  
  // Optional configuration
  baseUrl: 'https://sandbox.cardscan.ai/v1', // Override base URL
  websocketUrl: 'wss://sandbox.cardscan.ai/v1/ws', // WebSocket URL (required for fullScan)
  timeout: 30000, // Request timeout in milliseconds
  retries: 3, // Number of retries for failed requests
  retryDelay: 1000 // Delay between retries in milliseconds
});
```

## Browser Usage

The client works in both Node.js and browser environments:

```html
<!-- Using CDN -->
<script src="https://unpkg.com/@cardscan.ai/cardscan-client/dist/index.min.js"></script>
<script>
  const client = new CardScanApi.CardScanApi({ 
    apiKey: 'sk_test_cardscan_ai_...' 
  });
</script>
```

## Source Code

View the source code and contribute: [GitHub](https://github.com/CardScan-ai/api-clients/tree/main/clients/cardscan-ts)


# Python 🐍

The official Python client for the CardScan API provides a pythonic interface for all API operations.

## Installation

```bash
pip install cardscan-client
```

## Basic Usage

```python
from cardscan_client import CardScanApi
from cardscan_client.exceptions import ApiException

# Initialize with your API key
api_key = "sk_test_cardscan_ai_..."
client = CardScanApi(api_key=api_key)

# Generate a session token for a user
token_response = client.get_access_token(user_id="unique-user-123")
session_token = token_response["Token"]
identity_id = token_response["IdentityId"]
session_id = token_response["session_id"]

# Initialize client with session token for frontend operations
user_client = CardScanApi(session_token=session_token, live=False)
```

## Quick Start with full\_scan

The easiest way to scan cards is using the `full_scan` helper method that handles the entire workflow:

```python
from cardscan_client import CardScanApi

# Initialize the client
client = CardScanApi(api_key="sk_test_cardscan_ai_...")

# Scan a card (front and back)
async def scan_card():
    # Using file paths
    result = await client.full_scan(
        front_image="./front-card.jpg",
        back_image="./back-card.jpg",  # Optional - omit for front-only
        user_id="unique-user-123"
    )
    
    # Or using file objects
    with open("./front-card.jpg", "rb") as front:
        with open("./back-card.jpg", "rb") as back:
            result = await client.full_scan(
                front_image=front,
                back_image=back,
                user_id="unique-user-123"
            )
    
    # Access the results
    print(f"Card ID: {result['card_id']}")
    print(f"Member ID: {result['details']['member_id']}")
    print(f"Group: {result['details']['group_number']}")
    print(f"Payer: {result['details']['payer_name']}")
    
    return result

# Run the async function
import asyncio
result = asyncio.run(scan_card())
```

The `full_scan` method automatically:

* Generates a session token for the user
* Creates a card with appropriate settings
* Uploads images in the correct order
* Polls for processing completion
* Returns the completed card with all extracted data

## Manual Card Scanning Workflow

For more control over the scanning process, you can use the step-by-step approach:

### 1. Create a Card

```python
# Create a card with options
card = user_client.create_card(
    enable_backside_scan=False,
    enable_livescan=False,
    metadata={
        "patient_id": "12345",
        "visit_id": "v-67890"
    }
)

print(f"Card ID: {card['card_id']}")
print(f"State: {card['state']}")  # 'pending'
```

### 2. Generate Upload URL

```python
# Generate pre-signed upload URL
upload_data = user_client.generate_card_upload_url(
    card_id=card["card_id"],
    orientation="front",
    capture_type="manual"
)

upload_url = upload_data["upload_url"]
upload_parameters = upload_data["upload_parameters"]
```

### 3. Upload Image

```python
import requests

# Read image file
with open("insurance_card.jpg", "rb") as f:
    files = {"file": f}
    
    # Upload to S3 using pre-signed URL
    response = requests.post(
        upload_url,
        data=upload_parameters,
        files=files
    )
    response.raise_for_status()
```

### 4. Poll for Results

```python
import time

def wait_for_completion(card_id, timeout=300):
    """Poll for card completion with timeout."""
    start_time = time.time()
    
    while time.time() - start_time < timeout:
        card = user_client.get_card(card_id=card_id)
        
        if card["state"] in ["completed", "error"]:
            return card
            
        time.sleep(2)  # Wait 2 seconds between polls
    
    raise TimeoutError(f"Card {card_id} did not complete within {timeout} seconds")

completed_card = wait_for_completion(card["card_id"])
print("Insurance Info:", completed_card.get("details"))
```

## Error Handling

```python
from cardscan_client.exceptions import ApiException

try:
    card = client.get_card(card_id="invalid-id")
except ApiException as e:
    print(f"API Error: {e.status} - {e.body}")
    
    if e.status == 401:
        print("Invalid API key or session token")
    elif e.status == 404:
        print("Card not found")
    elif e.status == 429:
        print("Rate limit exceeded")
```

## Async Support

The client supports async operations using `asyncio`:

```python
import asyncio
from cardscan_client import AsyncCardScanApi

async def scan_card_async():
    async_client = AsyncCardScanApi(api_key="sk_test_cardscan_ai_...")
    
    # All methods are available as async versions
    token_response = await async_client.get_access_token(
        user_id="unique-user-123"
    )
    
    card = await async_client.create_card(
        session_token=token_response["Token"],
        enable_backside_scan=False
    )
    
    return card

# Run async function
card = asyncio.run(scan_card_async())
```

## WebSocket Support

For real-time updates using WebSocket:

```python
import websocket
import json

def on_message(ws, message):
    data = json.loads(message)
    
    if data["type"] == "card.processing":
        print("Card processing started")
    elif data["type"] == "card.completed":
        print(f"Card completed: {data['card_id']}")
    elif data["type"] == "card.error":
        print(f"Card error: {data['error']}")

def on_error(ws, error):
    print(f"WebSocket error: {error}")

# Connect to WebSocket
ws_url = f"wss://sandbox.cardscan.ai/v1/ws?token={session_token}"
ws = websocket.WebSocketApp(
    ws_url,
    on_message=on_message,
    on_error=on_error
)

# Run in a separate thread
import threading
wst = threading.Thread(target=ws.run_forever)
wst.daemon = True
wst.start()
```

## Eligibility Verification

```python
# Create eligibility request
eligibility = client.create_eligibility(
    card_id=completed_card["card_id"],
    eligibility={
        "provider": {
            "first_name": "John",
            "last_name": "Smith",
            "npi": "1234567890"
        },
        "subscriber": {
            "first_name": "Jane",
            "last_name": "Smith",
            "date_of_birth": "1980-01-01"
        }
    }
)

# Poll for eligibility results
eligibility_result = wait_for_eligibility_completion(
    eligibility["eligibility_id"]
)

# Access eligibility information
if eligibility_result["state"] == "completed":
    summary = eligibility_result["eligibility_summarized_response"]
    print(f"Coverage Active: {summary['coverage_active']}")
    print(f"Copay: ${summary['copay']}")
```

## Batch Operations

Process multiple cards efficiently:

```python
def process_cards_batch(image_paths):
    """Process multiple insurance cards in parallel."""
    cards = []
    
    # Create cards
    for path in image_paths:
        card = user_client.create_card(enable_backside_scan=False)
        cards.append(card)
    
    # Upload images in parallel
    import concurrent.futures
    
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        futures = []
        
        for card, image_path in zip(cards, image_paths):
            future = executor.submit(
                upload_and_process_card,
                card["card_id"],
                image_path
            )
            futures.append(future)
        
        # Wait for all uploads to complete
        results = [f.result() for f in futures]
    
    return results
```

## Configuration Options

```python
from cardscan_client import CardScanApi

# Custom configuration
client = CardScanApi(
    api_key="sk_test_cardscan_ai_...",
    base_url="https://sandbox.cardscan.ai/v1",
    retries=3,
    timeout=30,
    verify_ssl=True
)
```

## Type Hints

The client includes comprehensive type hints:

```python
from typing import Dict, Optional
from cardscan_client.models import (
    CardApiResponse,
    CreateCardRequest,
    EligibilityApiResponse
)

def process_card(card: CardApiResponse) -> Optional[Dict[str, str]]:
    """Extract key information from a completed card."""
    if card.state == "completed" and card.details:
        return {
            "member_id": card.details.member_id,
            "group_number": card.details.group_number,
            "payer_name": card.details.payer_name
        }
    return None
```

## Logging

Enable detailed logging for debugging:

```python
import logging

# Enable debug logging
logging.basicConfig(level=logging.DEBUG)

# The client will now log all requests and responses
client = CardScanApi(api_key="sk_test_cardscan_ai_...")
```

## Source Code

View the source code and contribute: [GitHub](https://github.com/CardScan-ai/api-clients/tree/main/clients/cardscan-python)


# Swift 🦉

The official Swift client for the CardScan API provides a native Swift interface with async/await support.

## Installation

### Swift Package Manager

Add the following to your `Package.swift`:

```swift
dependencies: [
    .package(url: "https://github.com/CardScan-ai/api-clients.git", from: "1.0.0")
]
```

Or in Xcode:

1. File → Add Package Dependencies
2. Enter: `https://github.com/CardScan-ai/api-clients.git`
3. Select version rule and add to your project

## Basic Usage

```swift
import CardScanClient

// Initialize with your API key
let apiKey = "sk_test_cardscan_ai_..."
let client = CardScanAPI(apiKey: apiKey)

// Generate a session token for a user
do {
    let tokenResponse = try await client.getAccessToken(userId: "unique-user-123")
    let sessionToken = tokenResponse.Token
    let identityId = tokenResponse.IdentityId
    let sessionId = tokenResponse.session_id
    
    // Initialize client with session token for frontend operations
    let userClient = CardScanAPI(sessionToken: sessionToken, live: false)
} catch {
    print("Error creating access token: \(error)")
}
```

## Card Scanning Workflow

### 1. Create a Card

```swift
struct CardCreationExample {
    let client: CardScanAPI
    
    func createCard() async throws -> CardApiResponse {
        let request = CreateCardRequest(
            enableBacksideScan: false,
            enableLivescan: false,
            metadata: [
                "patient_id": "12345",
                "visit_id": "v-67890"
            ]
        )
        
        let card = try await client.createCard(request: request)
        print("Card ID: \(card.cardId)")
        print("State: \(card.state)") // .pending
        
        return card
    }
}
```

### 2. Generate Upload URL

```swift
func generateUploadUrl(for cardId: String) async throws -> GenerateCardUploadUrlResponse {
    let request = GenerateCardUploadUrlRequest(
        orientation: .front,
        captureType: .manual
    )
    
    let uploadData = try await client.generateCardUploadUrl(
        cardId: cardId,
        request: request
    )
    
    return uploadData
}
```

### 3. Upload Image

```swift
import Foundation

func uploadImage(_ image: UIImage, using uploadData: GenerateCardUploadUrlResponse) async throws {
    guard let imageData = image.jpegData(compressionQuality: 0.8) else {
        throw CardScanError.invalidImage
    }
    
    var request = URLRequest(url: uploadData.uploadUrl)
    request.httpMethod = "POST"
    
    // Create multipart form data
    let boundary = UUID().uuidString
    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    
    var body = Data()
    
    // Add upload parameters
    for (key, value) in uploadData.uploadParameters {
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
        body.append("\(value)\r\n".data(using: .utf8)!)
    }
    
    // Add image data
    body.append("--\(boundary)\r\n".data(using: .utf8)!)
    body.append("Content-Disposition: form-data; name=\"file\"; filename=\"card.jpg\"\r\n".data(using: .utf8)!)
    body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
    body.append(imageData)
    body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
    
    request.httpBody = body
    
    let (_, response) = try await URLSession.shared.data(for: request)
    
    guard let httpResponse = response as? HTTPURLResponse,
          (200...299).contains(httpResponse.statusCode) else {
        throw CardScanError.uploadFailed
    }
}
```

### 4. Poll for Results

```swift
func waitForCompletion(cardId: String, timeout: TimeInterval = 300) async throws -> CardApiResponse {
    let startTime = Date()
    
    while Date().timeIntervalSince(startTime) < timeout {
        let card = try await client.getCard(cardId: cardId)
        
        switch card.state {
        case .completed, .error:
            return card
        default:
            // Wait 2 seconds before polling again
            try await Task.sleep(nanoseconds: 2_000_000_000)
        }
    }
    
    throw CardScanError.timeout
}

// Usage
let completedCard = try await waitForCompletion(cardId: card.cardId)
if let details = completedCard.details {
    print("Member ID: \(details.memberId ?? "N/A")")
    print("Group Number: \(details.groupNumber ?? "N/A")")
}
```

## Error Handling

```swift
enum CardScanError: Error {
    case invalidImage
    case uploadFailed
    case timeout
}

do {
    let card = try await client.getCard(cardId: "invalid-id")
} catch let error as ApiError {
    switch error.statusCode {
    case 401:
        print("Invalid API key or session token")
    case 404:
        print("Card not found")
    case 429:
        print("Rate limit exceeded")
    default:
        print("API Error: \(error.statusCode) - \(error.message)")
    }
} catch {
    print("Unexpected error: \(error)")
}
```

## WebSocket Support

For real-time updates:

```swift
import Foundation

class CardScanWebSocket: NSObject {
    private var webSocket: URLSessionWebSocketTask?
    private let session = URLSession(configuration: .default)
    private let token: String
    
    init(token: String) {
        self.token = token
        super.init()
    }
    
    func connect() {
        let url = URL(string: "wss://sandbox.cardscan.ai/v1/ws?token=\(token)")!
        webSocket = session.webSocketTask(with: url)
        webSocket?.resume()
        receiveMessage()
    }
    
    private func receiveMessage() {
        webSocket?.receive { [weak self] result in
            switch result {
            case .success(let message):
                switch message {
                case .string(let text):
                    self?.handleMessage(text)
                default:
                    break
                }
                self?.receiveMessage()
            case .failure(let error):
                print("WebSocket error: \(error)")
            }
        }
    }
    
    private func handleMessage(_ text: String) {
        guard let data = text.data(using: .utf8),
              let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
              let type = json["type"] as? String else { return }
        
        switch type {
        case "card.processing":
            print("Card processing started")
        case "card.completed":
            if let cardId = json["card_id"] as? String {
                print("Card completed: \(cardId)")
            }
        case "card.error":
            if let error = json["error"] as? String {
                print("Card error: \(error)")
            }
        default:
            break
        }
    }
}
```

## Eligibility Verification

```swift
func verifyEligibility(for card: CardApiResponse) async throws -> EligibilityApiResponse {
    let request = CreateEligibilityRequest(
        eligibility: EligibilityRequest(
            provider: ProviderDto(
                firstName: "John",
                lastName: "Smith",
                npi: "1234567890"
            ),
            subscriber: SubscriberDto(
                firstName: card.details?.memberName ?? "",
                lastName: card.details?.memberName ?? "",
                dateOfBirth: "1980-01-01"
            )
        )
    )
    
    let eligibility = try await client.createEligibility(
        cardId: card.cardId,
        request: request
    )
    
    // Poll for results
    return try await waitForEligibilityCompletion(
        eligibilityId: eligibility.eligibilityId
    )
}
```

## SwiftUI Integration

```swift
import SwiftUI
import CardScanClient

struct CardScannerView: View {
    @State private var isScanning = false
    @State private var scannedCard: CardApiResponse?
    @State private var error: Error?
    
    let client: CardScanAPI
    
    var body: some View {
        VStack {
            if let card = scannedCard {
                CardDetailsView(card: card)
            } else {
                Button("Scan Insurance Card") {
                    Task {
                        await scanCard()
                    }
                }
                .disabled(isScanning)
            }
            
            if isScanning {
                ProgressView("Processing...")
            }
            
            if let error = error {
                Text("Error: \(error.localizedDescription)")
                    .foregroundColor(.red)
            }
        }
        .padding()
    }
    
    @MainActor
    private func scanCard() async {
        isScanning = true
        error = nil
        
        do {
            // Create card
            let card = try await client.createCard(
                request: CreateCardRequest(enableBacksideScan: false)
            )
            
            // In a real app, capture/select image here
            // For demo, assume we have an image
            
            // Process and wait for results
            scannedCard = try await waitForCompletion(cardId: card.cardId)
        } catch {
            self.error = error
        }
        
        isScanning = false
    }
}
```

## Combine Support

For reactive programming with Combine:

```swift
import Combine

extension CardScanAPI {
    func createCardPublisher(request: CreateCardRequest) -> AnyPublisher<CardApiResponse, Error> {
        Future { promise in
            Task {
                do {
                    let card = try await self.createCard(request: request)
                    promise(.success(card))
                } catch {
                    promise(.failure(error))
                }
            }
        }
        .eraseToAnyPublisher()
    }
}

// Usage
let cancellable = client.createCardPublisher(request: request)
    .sink(
        receiveCompletion: { completion in
            if case .failure(let error) = completion {
                print("Error: \(error)")
            }
        },
        receiveValue: { card in
            print("Created card: \(card.cardId)")
        }
    )
```

## Configuration

```swift
// Custom configuration
let configuration = CardScanConfiguration(
    baseURL: "https://sandbox.cardscan.ai/v1",
    timeout: 30,
    retryCount: 3,
    retryDelay: 1.0
)

let client = CardScanAPI(
    apiKey: "sk_test_cardscan_ai_...",
    configuration: configuration
)
```

## Source Code

View the source code and contribute: [GitHub](https://github.com/CardScan-ai/api-clients/tree/main/clients/cardscan-swift)


# Kotlin 🟣

The official Kotlin client for the CardScan API provides a type-safe interface with coroutine support for Android and JVM applications.

## Installation

### Gradle (Kotlin DSL)

```kotlin
dependencies {
    implementation("com.cardscan:api:1.0.0")
}
```

### Gradle (Groovy)

```groovy
dependencies {
    implementation 'com.cardscan:api:1.0.0'
}
```

### Maven

```xml
<dependency>
    <groupId>com.cardscan</groupId>
    <artifactId>api</artifactId>
    <version>1.0.0</version>
</dependency>
```

## Basic Usage

```kotlin
import com.cardscan.api.CardScanApi
import com.cardscan.api.models.*

// Initialize with your API key
val apiKey = "sk_test_cardscan_ai_..."
val client = CardScanApi(apiKey)

// Generate a session token for a user
suspend fun authenticate(): String {
    val tokenResponse = client.getAccessToken(userId = "unique-user-123")
    val sessionToken = tokenResponse.Token
    val identityId = tokenResponse.IdentityId
    val sessionId = tokenResponse.session_id
    
    // Initialize client with session token for frontend operations
    val userClient = CardScanApi(sessionToken = sessionToken, live = false)
    
    return sessionToken
}
```

## Card Scanning Workflow

### 1. Create a Card

```kotlin
suspend fun createCard(): CardApiResponse {
    val request = CreateCardRequest(
        enableBacksideScan = false,
        enableLivescan = false,
        metadata = mapOf(
            "patient_id" to "12345",
            "visit_id" to "v-67890"
        )
    )
    
    val card = userClient.createCard(request)
    
    println("Card ID: ${card.cardId}")
    println("State: ${card.state}") // PENDING
    
    return card
}
```

### 2. Generate Upload URL

```kotlin
suspend fun generateUploadUrl(cardId: String): GenerateCardUploadUrlResponse {
    val request = GenerateCardUploadUrlRequest(
        orientation = ScanOrientation.FRONT,
        captureType = ScanCaptureType.MANUAL
    )
    
    val uploadData = userClient.generateCardUploadUrl(
        cardId = cardId,
        request = request
    )
    
    return uploadData
}
```

### 3. Upload Image

```kotlin
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import java.io.File

suspend fun uploadImage(
    imageFile: File, 
    uploadData: GenerateCardUploadUrlResponse
) {
    val client = OkHttpClient()
    
    val multipartBody = MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .apply {
            // Add upload parameters
            uploadData.uploadParameters.forEach { (key, value) ->
                addFormDataPart(key, value)
            }
            
            // Add image file
            addFormDataPart(
                "file",
                imageFile.name,
                RequestBody.create(
                    "image/jpeg".toMediaType(),
                    imageFile
                )
            )
        }
        .build()
    
    val request = Request.Builder()
        .url(uploadData.uploadUrl)
        .post(multipartBody)
        .build()
    
    client.newCall(request).execute().use { response ->
        if (!response.isSuccessful) {
            throw IOException("Upload failed: ${response.code}")
        }
    }
}
```

### 4. Poll for Results

```kotlin
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds

suspend fun waitForCompletion(
    cardId: String,
    timeout: Duration = 5.minutes
): CardApiResponse {
    return withTimeout(timeout) {
        while (true) {
            val card = userClient.getCard(cardId)
            
            when (card.state) {
                CardState.COMPLETED, CardState.ERROR -> return@withTimeout card
                else -> delay(2.seconds)
            }
        }
    }
}

// Usage
val completedCard = waitForCompletion(card.cardId)
completedCard.details?.let { details ->
    println("Member ID: ${details.memberId}")
    println("Group Number: ${details.groupNumber}")
}
```

## Error Handling

```kotlin
import com.cardscan.api.exceptions.ApiException

try {
    val card = client.getCard("invalid-id")
} catch (e: ApiException) {
    when (e.statusCode) {
        401 -> println("Invalid API key or session token")
        404 -> println("Card not found")
        429 -> println("Rate limit exceeded")
        else -> println("API Error: ${e.statusCode} - ${e.message}")
    }
} catch (e: Exception) {
    println("Unexpected error: ${e.message}")
}
```

## Android Integration

### Camera Capture

```kotlin
import android.content.Context
import android.graphics.Bitmap
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider

class CardScanner(private val context: Context) {
    private val client = CardScanApi(sessionToken = "tok_...")
    
    suspend fun scanCard(bitmap: Bitmap): CardApiResponse {
        // Create card
        val card = client.createCard(
            CreateCardRequest(enableBacksideScan = false)
        )
        
        // Convert bitmap to file
        val imageFile = saveBitmapToFile(bitmap)
        
        // Get upload URL
        val uploadData = client.generateCardUploadUrl(
            cardId = card.cardId,
            request = GenerateCardUploadUrlRequest(
                orientation = ScanOrientation.FRONT
            )
        )
        
        // Upload image
        uploadImage(imageFile, uploadData)
        
        // Wait for processing
        return waitForCompletion(card.cardId)
    }
    
    private fun saveBitmapToFile(bitmap: Bitmap): File {
        val file = File(context.cacheDir, "card_${System.currentTimeMillis()}.jpg")
        file.outputStream().use { out ->
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
        }
        return file
    }
}
```

### WebSocket Support

```kotlin
import okhttp3.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

class CardScanWebSocket(private val token: String) {
    private val client = OkHttpClient()
    private var webSocket: WebSocket? = null
    private val events = Channel<CardWebsocketEvent>()
    
    fun connect(): Flow<CardWebsocketEvent> = flow {
        val request = Request.Builder()
            .url("wss://sandbox.cardscan.ai/v1/ws?token=$token")
            .build()
        
        webSocket = client.newWebSocket(request, object : WebSocketListener() {
            override fun onMessage(webSocket: WebSocket, text: String) {
                val event = parseWebSocketEvent(text)
                events.trySend(event)
            }
            
            override fun onFailure(
                webSocket: WebSocket,
                t: Throwable,
                response: Response?
            ) {
                events.close(t)
            }
        })
        
        // Emit events from channel
        for (event in events) {
            emit(event)
        }
    }
    
    fun disconnect() {
        webSocket?.close(1000, "Client disconnect")
        events.close()
    }
}

// Usage with Flow
cardScanWebSocket.connect().collect { event ->
    when (event.type) {
        "card.processing" -> println("Processing started")
        "card.completed" -> println("Card completed: ${event.cardId}")
        "card.error" -> println("Error: ${event.error}")
    }
}
```

## Eligibility Verification

```kotlin
suspend fun verifyEligibility(card: CardApiResponse): EligibilityApiResponse {
    val request = CreateEligibilityRequest(
        eligibility = EligibilityRequest(
            provider = ProviderDto(
                firstName = "John",
                lastName = "Smith",
                npi = "1234567890"
            ),
            subscriber = SubscriberDto(
                firstName = card.details?.memberName ?: "",
                lastName = card.details?.memberName ?: "",
                dateOfBirth = "1980-01-01"
            )
        )
    )
    
    val eligibility = client.createEligibility(
        cardId = card.cardId,
        request = request
    )
    
    // Poll for results
    return waitForEligibilityCompletion(eligibility.eligibilityId)
}
```

## Jetpack Compose Integration

```kotlin
import androidx.compose.runtime.*
import androidx.compose.material3.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class CardScanViewModel : ViewModel() {
    private val client = CardScanApi(sessionToken = "tok_...")
    
    private val _uiState = MutableStateFlow(CardScanUiState())
    val uiState: StateFlow<CardScanUiState> = _uiState
    
    fun scanCard(imageFile: File) {
        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(isLoading = true)
            
            try {
                // Create card
                val card = client.createCard(
                    CreateCardRequest(enableBacksideScan = false)
                )
                
                // Generate upload URL
                val uploadData = client.generateCardUploadUrl(
                    cardId = card.cardId,
                    request = GenerateCardUploadUrlRequest(
                        orientation = ScanOrientation.FRONT
                    )
                )
                
                // Upload image
                uploadImage(imageFile, uploadData)
                
                // Wait for results
                val completedCard = waitForCompletion(card.cardId)
                
                _uiState.value = _uiState.value.copy(
                    isLoading = false,
                    card = completedCard
                )
            } catch (e: Exception) {
                _uiState.value = _uiState.value.copy(
                    isLoading = false,
                    error = e.message
                )
            }
        }
    }
}

@Composable
fun CardScanScreen(viewModel: CardScanViewModel) {
    val uiState by viewModel.uiState.collectAsState()
    
    Column {
        if (uiState.isLoading) {
            CircularProgressIndicator()
        }
        
        uiState.card?.let { card ->
            Card {
                Column(modifier = Modifier.padding(16.dp)) {
                    Text("Member ID: ${card.details?.memberId ?: "N/A"}")
                    Text("Group: ${card.details?.groupNumber ?: "N/A"}")
                    Text("Payer: ${card.details?.payerName ?: "N/A"}")
                }
            }
        }
        
        uiState.error?.let { error ->
            Text(
                text = "Error: $error",
                color = MaterialTheme.colorScheme.error
            )
        }
        
        Button(
            onClick = { /* Trigger image capture */ },
            enabled = !uiState.isLoading
        ) {
            Text("Scan Card")
        }
    }
}

data class CardScanUiState(
    val isLoading: Boolean = false,
    val card: CardApiResponse? = null,
    val error: String? = null
)
```

## Configuration

```kotlin
// Custom configuration
val config = CardScanConfiguration(
    baseUrl = "https://sandbox.cardscan.ai/v1",
    timeout = 30_000, // milliseconds
    retryCount = 3,
    retryDelay = 1_000 // milliseconds
)

val client = CardScanApi(
    apiKey = "sk_test_cardscan_ai_...",
    configuration = config
)

// With OkHttp interceptors
val okHttpClient = OkHttpClient.Builder()
    .addInterceptor { chain ->
        val request = chain.request().newBuilder()
            .addHeader("X-Custom-Header", "value")
            .build()
        chain.proceed(request)
    }
    .build()

val clientWithCustomHttp = CardScanApi(
    apiKey = apiKey,
    httpClient = okHttpClient
)
```

## Testing

```kotlin
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test

class CardScanTest {
    private val mockClient = mockk<CardScanApi>()
    
    @Test
    fun `test card creation`() = runTest {
        val expectedCard = CardApiResponse(
            cardId = "test-id",
            state = CardState.PENDING
        )
        
        coEvery { 
            mockClient.createCard(any()) 
        } returns expectedCard
        
        val card = mockClient.createCard(
            CreateCardRequest(enableBacksideScan = false)
        )
        
        assert(card.cardId == "test-id")
        assert(card.state == CardState.PENDING)
    }
}
```

## Source Code

View the source code and contribute: [GitHub](https://github.com/CardScan-ai/api-clients/tree/main/clients/cardscan-kotlin)


# Dart 🎯

The official Dart client for the CardScan API provides support for Flutter, web, and server-side Dart applications.

## Installation

Add to your `pubspec.yaml`:

```yaml
dependencies:
  cardscan_client: ^1.0.0
```

Then run:

```bash
flutter pub get
# or for Dart-only projects
dart pub get
```

## Basic Usage

```dart
import 'package:cardscan_client/cardscan_client.dart';

// Initialize with your API key
final apiKey = 'sk_test_cardscan_ai_...';
final client = CardScanApi(apiKey: apiKey);

// Generate a session token for a user
Future<void> authenticate() async {
  final tokenResponse = await client.getAccessToken(
    userId: 'unique-user-123',
  );
  
  final sessionToken = tokenResponse['Token'];
  final identityId = tokenResponse['IdentityId'];
  final sessionId = tokenResponse['session_id'];
  
  // Initialize client with session token for frontend operations
  final userClient = CardScanApi(
    sessionToken: sessionToken,
    live: false, // Use sandbox
  );
}
```

## Card Scanning Workflow

### 1. Create a Card

```dart
Future<CardApiResponse> createCard() async {
  final card = await userClient.createCard(
    CreateCardRequest(
      enableBacksideScan: false,
      enableLivescan: false,
      metadata: {
        'patient_id': '12345',
        'visit_id': 'v-67890',
      },
    ),
  );
  
  print('Card ID: ${card.cardId}');
  print('State: ${card.state}'); // CardState.pending
  
  return card;
}
```

### 2. Generate Upload URL

```dart
Future<GenerateCardUploadUrlResponse> generateUploadUrl(String cardId) async {
  final uploadData = await userClient.generateCardUploadUrl(
    cardId,
    GenerateCardUploadUrlRequest(
      orientation: ScanOrientation.front,
      captureType: ScanCaptureType.manual,
    ),
  );
  
  return uploadData;
}
```

### 3. Upload Image

```dart
import 'dart:io';
import 'package:dio/dio.dart';

Future<void> uploadImage(
  File imageFile,
  GenerateCardUploadUrlResponse uploadData,
) async {
  final dio = Dio();
  
  final formData = FormData();
  
  // Add upload parameters
  uploadData.uploadParameters.forEach((key, value) {
    formData.fields.add(MapEntry(key, value));
  });
  
  // Add image file
  formData.files.add(MapEntry(
    'file',
    await MultipartFile.fromFile(
      imageFile.path,
      filename: 'card.jpg',
    ),
  ));
  
  final response = await dio.post(
    uploadData.uploadUrl,
    data: formData,
  );
  
  if (response.statusCode != 204) {
    throw Exception('Upload failed: ${response.statusCode}');
  }
}
```

### 4. Poll for Results

```dart
Future<CardApiResponse> waitForCompletion(
  String cardId, {
  Duration timeout = const Duration(minutes: 5),
}) async {
  final startTime = DateTime.now();
  
  while (DateTime.now().difference(startTime) < timeout) {
    final card = await userClient.getCard(cardId);
    
    if (card.state == CardState.completed || 
        card.state == CardState.error) {
      return card;
    }
    
    await Future.delayed(const Duration(seconds: 2));
  }
  
  throw TimeoutException('Card processing timed out');
}

// Usage
final completedCard = await waitForCompletion(card.cardId);
if (completedCard.details != null) {
  print('Member ID: ${completedCard.details!.memberId}');
  print('Group Number: ${completedCard.details!.groupNumber}');
}
```

## Error Handling

```dart
import 'package:cardscan_client/cardscan_client.dart';

try {
  final card = await client.getCard('invalid-id');
} on ApiException catch (e) {
  switch (e.statusCode) {
    case 401:
      print('Invalid API key or session token');
      break;
    case 404:
      print('Card not found');
      break;
    case 429:
      print('Rate limit exceeded');
      break;
    default:
      print('API Error: ${e.statusCode} - ${e.message}');
  }
} catch (e) {
  print('Unexpected error: $e');
}
```

## Flutter Integration

### Camera Capture with image\_picker

```dart
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:cardscan_client/cardscan_client.dart';

class CardScannerScreen extends StatefulWidget {
  @override
  _CardScannerScreenState createState() => _CardScannerScreenState();
}

class _CardScannerScreenState extends State<CardScannerScreen> {
  final client = CardScanApi(sessionToken: 'tok_...');
  final picker = ImagePicker();
  
  bool _isLoading = false;
  CardApiResponse? _scannedCard;
  String? _error;
  
  Future<void> _scanCard() async {
    setState(() {
      _isLoading = true;
      _error = null;
    });
    
    try {
      // Capture image
      final image = await picker.pickImage(
        source: ImageSource.camera,
        maxWidth: 1920,
        maxHeight: 1080,
        imageQuality: 90,
      );
      
      if (image == null) {
        setState(() => _isLoading = false);
        return;
      }
      
      // Create card
      final card = await client.createCard(
        CreateCardRequest(enableBacksideScan: false),
      );
      
      // Generate upload URL
      final uploadData = await client.generateCardUploadUrl(
        card.cardId,
        GenerateCardUploadUrlRequest(
          orientation: ScanOrientation.front,
        ),
      );
      
      // Upload image
      await uploadImage(File(image.path), uploadData);
      
      // Wait for results
      final completedCard = await waitForCompletion(card.cardId);
      
      setState(() {
        _scannedCard = completedCard;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Scan Insurance Card')),
      body: Center(
        child: _isLoading
            ? CircularProgressIndicator()
            : _scannedCard != null
                ? _buildCardDetails()
                : _buildScanButton(),
      ),
    );
  }
  
  Widget _buildCardDetails() {
    final details = _scannedCard!.details;
    
    return Card(
      margin: EdgeInsets.all(16),
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('Member ID: ${details?.memberId ?? 'N/A'}'),
            Text('Group: ${details?.groupNumber ?? 'N/A'}'),
            Text('Payer: ${details?.payerName ?? 'N/A'}'),
            if (details?.copayEr != null)
              Text('ER Copay: \$${details!.copayEr}'),
          ],
        ),
      ),
    );
  }
  
  Widget _buildScanButton() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        ElevatedButton.icon(
          onPressed: _scanCard,
          icon: Icon(Icons.camera_alt),
          label: Text('Scan Card'),
        ),
        if (_error != null)
          Padding(
            padding: EdgeInsets.only(top: 16),
            child: Text(
              _error!,
              style: TextStyle(color: Colors.red),
            ),
          ),
      ],
    );
  }
}
```

### WebSocket Support

```dart
import 'package:web_socket_channel/web_socket_channel.dart';
import 'dart:convert';

class CardScanWebSocket {
  final String token;
  WebSocketChannel? _channel;
  
  CardScanWebSocket({required this.token});
  
  Stream<CardWebsocketEvent> connect() {
    _channel = WebSocketChannel.connect(
      Uri.parse('wss://sandbox.cardscan.ai/v1/ws?token=$token'),
    );
    
    return _channel!.stream.map((data) {
      final json = jsonDecode(data);
      return CardWebsocketEvent.fromJson(json);
    });
  }
  
  void disconnect() {
    _channel?.sink.close();
  }
}

// Usage
final websocket = CardScanWebSocket(token: sessionToken);

websocket.connect().listen((event) {
  switch (event.type) {
    case 'card.processing':
      print('Processing started');
      break;
    case 'card.completed':
      print('Card completed: ${event.cardId}');
      break;
    case 'card.error':
      print('Error: ${event.error}');
      break;
  }
});
```

## Eligibility Verification

```dart
Future<EligibilityApiResponse> verifyEligibility(
  CardApiResponse card,
) async {
  final request = CreateEligibilityRequest(
    eligibility: EligibilityRequest(
      provider: ProviderDto(
        firstName: 'John',
        lastName: 'Smith',
        npi: '1234567890',
      ),
      subscriber: SubscriberDto(
        firstName: card.details?.memberName ?? '',
        lastName: card.details?.memberName ?? '',
        dateOfBirth: '1980-01-01',
      ),
    ),
  );
  
  final eligibility = await client.createEligibility(
    card.cardId,
    request,
  );
  
  // Poll for results
  return await waitForEligibilityCompletion(
    eligibility.eligibilityId,
  );
}
```

## State Management with Riverpod

```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cardscan_client/cardscan_client.dart';

// Providers
final cardScanApiProvider = Provider<CardScanApi>((ref) {
  return CardScanApi(sessionToken: 'tok_...');
});

final currentCardProvider = StateNotifierProvider<CardNotifier, AsyncValue<CardApiResponse?>>((ref) {
  return CardNotifier(ref.read(cardScanApiProvider));
});

// State Notifier
class CardNotifier extends StateNotifier<AsyncValue<CardApiResponse?>> {
  final CardScanApi _api;
  
  CardNotifier(this._api) : super(AsyncValue.data(null));
  
  Future<void> scanCard(File imageFile) async {
    state = AsyncValue.loading();
    
    try {
      // Create card
      final card = await _api.createCard(
        CreateCardRequest(enableBacksideScan: false),
      );
      
      // Generate upload URL
      final uploadData = await _api.generateCardUploadUrl(
        card.cardId,
        GenerateCardUploadUrlRequest(
          orientation: ScanOrientation.front,
        ),
      );
      
      // Upload image
      await uploadImage(imageFile, uploadData);
      
      // Wait for results
      final completedCard = await waitForCompletion(card.cardId);
      
      state = AsyncValue.data(completedCard);
    } catch (e, stack) {
      state = AsyncValue.error(e, stack);
    }
  }
}

// Widget
class CardScanView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final cardState = ref.watch(currentCardProvider);
    
    return cardState.when(
      data: (card) => card != null
          ? CardDetailsWidget(card: card)
          : ScanButton(
              onPressed: () => _pickAndScanImage(ref),
            ),
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => ErrorWidget(error),
    );
  }
  
  Future<void> _pickAndScanImage(WidgetRef ref) async {
    final picker = ImagePicker();
    final image = await picker.pickImage(source: ImageSource.camera);
    
    if (image != null) {
      ref.read(currentCardProvider.notifier).scanCard(File(image.path));
    }
  }
}
```

## Web Support

For Flutter Web, use the HTML file input:

```dart
import 'dart:html' as html;
import 'dart:typed_data';

Future<void> uploadImageWeb(
  Uint8List imageBytes,
  GenerateCardUploadUrlResponse uploadData,
) async {
  final formData = html.FormData();
  
  // Add upload parameters
  uploadData.uploadParameters.forEach((key, value) {
    formData.append(key, value);
  });
  
  // Create blob from bytes
  final blob = html.Blob([imageBytes], 'image/jpeg');
  formData.appendBlob('file', blob, 'card.jpg');
  
  // Upload using Fetch API
  final response = await html.HttpRequest.request(
    uploadData.uploadUrl,
    method: 'POST',
    sendData: formData,
  );
  
  if (response.status != 204) {
    throw Exception('Upload failed: ${response.status}');
  }
}
```

## Configuration

```dart
// Custom configuration
final client = CardScanApi(
  apiKey: 'sk_test_cardscan_ai_...',
  baseUrl: 'https://sandbox.cardscan.ai/v1',
  timeout: Duration(seconds: 30),
  retryAttempts: 3,
  retryDelay: Duration(seconds: 1),
);

// With custom HTTP client
import 'package:dio/dio.dart';

final dio = Dio()
  ..interceptors.add(LogInterceptor())
  ..options.headers['X-Custom-Header'] = 'value';

final clientWithCustomHttp = CardScanApi(
  apiKey: apiKey,
  httpClient: dio,
);
```

## Testing

```dart
import 'package:test/test.dart';
import 'package:mockito/mockito.dart';
import 'package:cardscan_client/cardscan_client.dart';

class MockCardScanApi extends Mock implements CardScanApi {}

void main() {
  group('CardScan Tests', () {
    late MockCardScanApi mockApi;
    
    setUp(() {
      mockApi = MockCardScanApi();
    });
    
    test('creates card successfully', () async {
      final expectedCard = CardApiResponse(
        cardId: 'test-id',
        state: CardState.pending,
      );
      
      when(mockApi.createCard(any)).thenAnswer(
        (_) async => expectedCard,
      );
      
      final card = await mockApi.createCard(
        CreateCardRequest(enableBacksideScan: false),
      );
      
      expect(card.cardId, equals('test-id'));
      expect(card.state, equals(CardState.pending));
    });
  });
}
```

## Source Code

View the source code and contribute: [GitHub](https://github.com/CardScan-ai/api-clients/tree/main/clients/cardscan-dart)


# Enriched Results 🔍

CardScan's AI-powered enrichment extracts additional structured data from insurance cards beyond the standard OCR processing. This includes labeled phone numbers, addresses, and copayment information with high confidence scores ready to be used in your business processes.

## Overview

The enriched results feature uses advanced AI models to perform sophisticated extraction from insurance card images. This is a secondary process that runs after the standard card scanning is completed, providing additional data points with enhanced accuracy.

**Important:** Enriched results processing uses a secondary model processing pipeline with about 8 seconds of latency after standard card processing completes. Your application should handle both the initial `completed` state and the subsequent `enriched` state with enhanced data.

## Use Cases

**Automated Benefits Verification:** Extract high-reliability `provider_services_phone` numbers from insurance cards to use with manual or AI-automated voice benefits checking systems, eliminating the need to manually search for correct provider contact numbers.

**Claims Processing Automation:** Automatically route claims to the correct mailing addresses by extracting labeled `medical_claims_address` and `pharmacy_claims_address` information, reducing processing delays and improving accuracy in claims submission workflows.

**Payer Deduplication:** De-duplicate payers using extracted claims addresses and member services phone numbers to identify when different insurance plans belong to the same parent organization, improving data quality and reducing redundant payer records in your system.

## Accessing Enriched Results

### API Response Structure

When a card reaches the `enriched` state, the enriched data is available in the `enriched_results` field:

```json
{
  "card_id": "01234567-89ab-cdef-0123-456789abcdef",
  "state": "enriched",
  "enriched_results": {
    "addresses": [
      {
        "label": "Send Claims to",
        "type": "medical_claims_address",
        "company_name": "ABC Health Plans",
        "address": "PO Box 9999, Phoenix, AZ 85001",
        "score": "0.990"
      }
    ],
    "phone_numbers": [
      {
        "label": "Member Services",
        "type": "member_services_phone",
        "number": "555-123-4567",
        "score": "0.980"
      }
    ],
    "copays_deductibles": [
      {
        "service": "office_visit",
        "category": "copay",
        "value": 40,
        "score": "0.950"
      }
    ],
    "processed_sides": "both"
  }
}
```

### Field Descriptions

#### Address Fields

* `label`: Descriptive text found on the card (e.g., "Send Claims to", "Mail Appeals to")
* `type`: Standardized address category
* `company_name`: Organization or company name associated with the address
* `address`: The actual mailing address
* `score`: Confidence score as a string with 3 decimal places (0.000-1.000)

#### Phone Number Fields

* `label`: Descriptive text found on the card (e.g., "Member Services", "For prior-auth call")
* `type`: Standardized phone number category
* `number`: Phone number in NPA-NXX-XXXX format (e.g., "415-555-1212")
* `score`: Confidence score as a string with 3 decimal places (0.000-1.000)

#### Copay/Deductible Fields

* `service`: Type of medical service
* `category`: Cost type (copay, coinsurance, deductible, out\_of\_pocket\_max)
* `value`: Numeric amount
* `score`: Confidence score as a string with 3 decimal places (0.000-1.000)

#### Processing Indicator

* `processed_sides`: Indicates whether AI processing analyzed "front\_only" or "both" sides of the card

## Address Types

| Type                      | Description                              |
| ------------------------- | ---------------------------------------- |
| `medical_claims_address`  | Address for submitting medical claims    |
| `pharmacy_claims_address` | Address for pharmacy/prescription claims |
| `vision_claims_address`   | Address for vision benefit claims        |
| `dental_claims_address`   | Address for dental benefit claims        |
| `appeals_address`         | Address for appeals and grievances       |
| `pcp_address`             | Primary care provider office address     |
| `general_address`         | General contact or corporate address     |

## Phone Number Types

| Type                      | Description                        |
| ------------------------- | ---------------------------------- |
| `pcp_phone`               | Primary care provider phone number |
| `member_services_phone`   | Member/customer service line       |
| `provider_services_phone` | Provider services phone number     |
| `pharmacy_services_phone` | Pharmacy benefit phone number      |
| `dental_benefit_phone`    | Dental benefits phone number       |
| `vision_benefit_phone`    | Vision benefits phone number       |
| `telemedicine_phone`      | Telemedicine services phone number |

## Copay Services

| Service                  | Description                     |
| ------------------------ | ------------------------------- |
| `office_visit`           | Primary care office visits      |
| `specialist_visit`       | Specialist consultations        |
| `emergency_room`         | Emergency room visits           |
| `urgent_care`            | Urgent care visits              |
| `preventive_care`        | Preventive care services        |
| `prescription`           | Prescription medications        |
| `vision`                 | Vision care services            |
| `dental`                 | Dental care services            |
| `telemedicine`           | Telemedicine consultations      |
| `in_network_medical`     | In-network medical services     |
| `out_of_network_medical` | Out-of-network medical services |
| `other`                  | Other medical services          |

## Copay Categories

| Category            | Description                     |
| ------------------- | ------------------------------- |
| `copay`             | Fixed dollar amount copayment   |
| `coinsurance`       | Percentage-based cost sharing   |
| `deductible`        | Annual deductible amount        |
| `out_of_pocket_max` | Annual out-of-pocket maximum    |
| `other`             | Other cost-sharing arrangements |

## Card States and Processing

### Sequential Processing Flow

CardScan uses a two-stage processing pipeline:

1. **Primary Processing**:
   * `pending` → Card created, awaiting processing
   * `processing` → OCR and ML processing in progress
   * `completed` → Standard processing finished, basic card data available
2. **Secondary Processing** (approximately 8 seconds additional):
   * `enriched` → AI enrichment completed, enhanced data available (final state)

### Processing Timeline

* **Standard card details** (member numbers, payer info, etc.) are available when state reaches `completed`
* **Enriched results** (labeled phone numbers, addresses, copays) are available when state reaches `enriched`
* **Background processing**: Enrichment runs automatically after completion without blocking the primary workflow

## Feature Availability

### Feature Flag

Enriched results require the `post_processing_enriched_results` feature flag to be enabled for your account. Contact support to enable this feature for free.

## Getting Notified of Enrichment Completion

Once a card has finished enrichment processing, you can be notified through several methods:

### Webhook Events

When enrichment completes, a `card.enriched` webhook event is triggered. See the [Webhooks documentation](/advanced-features/webhooks) for setup details.

```json
{
  "type": "card.enriched",
  "card_id": "01234567-89ab-cdef-0123-456789abcdef",
  "created_at": "2024-01-15T10:30:00Z",
  "enriched_results": {
    "addresses": [...],
    "phone_numbers": [...],
    "copays_deductibles": [...]
  }
}
```

### WebSocket Updates

Use the client APIs to listen for real-time WebSocket changes when the card state transitions to `enriched`.

### Polling

Poll the card endpoint periodically to check when the state changes from `completed` to `enriched`.

## Best Practices

### Confidence Scores

* Scores above 0.900 are typically very reliable
* Scores between 0.700-0.900 should be validated
* Scores below 0.700 may require manual verification
* All scores are returned as strings with 3 decimal places (e.g., "0.950", "0.123")

### Error Handling

```javascript
// Always check if enriched_results exists
if (card.enrichedResults) {
  // Process enriched data
  const phoneNumbers = card.enrichedResults.phoneNumbers || [];
  const addresses = card.enrichedResults.addresses || [];
  const copays = card.enrichedResults.copaysDeductibles || [];
}
```

## Troubleshooting

### Common Issues

**Enriched results not appearing:**

* Verify the `post_processing_enriched_results` feature flag is enabled
* Check that the card state has reached `enriched`
* Some cards may not have enrichable data (empty arrays are normal)

**Missing data:**

* Not all cards contain phone numbers, addresses, or copay information
* Empty arrays in `enriched_results` indicate no extractable data was found
* Low confidence scores may indicate unclear or damaged card images

**Processing delays:**

* Enrichment requires additional AI processing time
* Complex cards with multiple sides take longer to process
* Network latency can affect processing times

For additional support with enriched results, contact our developer support team.


# Eligibility Verification 🩻

<figure><img src="/files/oj4gZ1YtUghNxFM6m0eq" alt="" width="563"><figcaption><p>Sample Eligibility Response</p></figcaption></figure>

## Getting Started

### What is Eligibility Verification?

Eligibility Verification is one of the most important business processes in healthcare, ensuring patients' insurance coverage is confirmed before services are provided.

This process is crucial for smooth **provider reimbursement** and reducing billing surprises for patients, ultimately enhancing their **access to care**. It's a key step in delivering efficient, accessible healthcare services.

### What information does it provide?

* **Coverage Status**: Confirms whether the patient's insurance is currently active or inactive.
* **Benefit Details**: Outlines specific benefits covered under the patient's insurance plan, including types of services (e.g., outpatient, inpatient, laboratory services).
* **Deductible Information**: Details about the patient's deductible, including how much has been met and the remaining amount.
* **Co-pay Amounts**: Information on required co-pay amounts for different services (e.g., specialist visits, emergency room visits).
* **Co-insurance Rates**: Percentage of costs that the patient is responsible for after the deductible is met.
* **Out-of-Pocket Maximums**: The maximum amount the patient is required to pay out-of-pocket during a policy period.
* **Prior Authorization Requirements**: Indicates if certain services require prior authorization from the insurance provider.
* **Service Limitations**: Information on any limits to services, such as a cap on the number of visits or specific exclusions.
* **Network Status**: Confirmation of whether a provider or service is in-network or out-of-network, which affects the patient's costs.

### Prerequisites

Ready to get started? There are a few requirements to be able to use eligibility successfully.

* [x] Active CardScan.ai account with the eligibility feature enabled.
* [x] NPI of the provider or organization who will be providing the service.
* [x] Basic demographics for the **patient of interest.**

{% hint style="warning" %}
**Note:** Eligibility verification is a paid add-on feature. Please ensure that you have the appropriate subscription plan to access this functionality.
{% endhint %}

## Integration

### **UI Widgets**

For easy integration of the eligibility feature, developers can utilize our UI widgets. The React component and mobile SDKs provide straightforward methods to perform eligibility checks alongside the insurance card scanning process.

To trigger an eligibility request using our UI widgets, simply pass in the eligibility request payload as described below. The payload must include subscriber and dependent demographics, as well as provider details, including a National Provider Identifier (NPI). This information is crucial for accurately verifying insurance eligibility.

#### **Sample Request Payload:**

```json
{
    "card_id": "c1b93738-ddc0-4beb-9936-1f93fe0e4279",
    "eligibility": {
        "subscriber": {
            "firstName": "John",
            "lastName": "Doe",
            "dateOfBirth": "18020101"
        },
        "provider": {
            "firstName": "Jane",
            "lastName": "Doe",
            "npi": "1952535221"
        }
    }
}
```

#### **Alternative Provider Format:**

```json
{
    "card_id": "c1b93738-ddc0-4beb-9936-1f93fe0e4279",
    "eligibility": {
        "subscriber": {
            "firstName": "John",
            "lastName": "Doe",
            "dateOfBirth": "18020101"
        },
        "provider": {
            "organizationName": "STANFORD HEALTH CARE",
            "npi": "1871543215"
        }
    }
}
```

You can locate a providers NPI on the [NPPES NPI Registry](https://npiregistry.cms.hhs.gov/search%23pageStart) search page.

{% hint style="info" %}
Date of birth is required to be in the\*\*`YYYYMMDD`\*\*format.
{% endhint %}

#### **Callbacks**

Our UI widgets provide two additional callbacks for handling eligibility verification results. Developers can implement `onEligibilitySuccess` and `onEligibilityError` to receive notifications when an eligibility check is successful or encounters an error, respectively.

#### **React Example:**

```jsx
import { CardScanView } from "@cardscan.ai/insurance-cardscan-react";

function EligibilityApp({ apiKey, ...props }) {

  const eligibility = {
    subscriber: {
      firstName: "Joe",
      lastName: "Doe",
      dateOfBirth: "18020101",
    },
    provider: {
      firstName: "John",
      lastName: "Doe",
      npi: "0123456789",
    },
  };

  function cardScanSuccess(card) {
    console.log(card);
  }

  function cardScanCancel() {
    setShowScanView(false);
  }

  function cardScanError(error) {
    console.log("Error Callback: ", error);
  }

  function eligibilitySuccess(eligibility) {
    console.log(eligibility);
  }

  function eligibilityError(error) {
    console.log(error);
  }

  return (
    <div className="App">
      <header className="App-header">
        <CardScanView
          sessionToken={'<YOUR SESSION TOKEN>'}
          onSuccess={cardScanSuccess}
          onCancel={cardScanCancel}
          onError={cardScanError}
          eligibility={eligibility}
          backsideSupport={true}
          onEligibilitySuccess={eligibilitySuccess}
          onEligibilityError={eligibilityError}
        />
      </header>
    </div>
  );
}

export default EligibilityApp;
```

### **API Integration**

For custom integrations, our Eligibility Verification Endpoints are designed to work in tandem with our Insurance Card Scanning Endpoints. By creating a new eligibility request, users can initiate an asynchronous process that verifies insurance coverage and benefits.

Please checkout the [API 💻](/api#eligibility-verification) section of our API documentation for endpoint details and a **Postman** collection.

#### Sample Request Payload

To initiate an eligibility request through our API endpoints, two key pieces of information are required:

1. **Eligibility Request Payload:** This includes subscriber and dependent demographics, along with provider details, including a National Provider Identifier (NPI).
2. **Card ID:** The ID of a card in the `completed` state must also be provided to ensure the request is linked to the correct insurance card information.

{% hint style="info" %}
If you would like to use the API directly without first having scanned an insurance card, please reach out to support.
{% endhint %}

```json
{
    "card_id": "cdeef0bd-f170-44fb-95a4-8d4dfb45bdfc",
    "eligibility": {
        "subscriber": {
            "firstName": "John",
            "lastName": "Doe",
            "dateOfBirth": "18020101"
        },
        "provider": {
            "firstName": "John",
            "lastName": "Doe",
            "npi": "0123456789"
        }
    }
}
```

You can locate a providers NPI on the [NPPES NPI Registry](https://npiregistry.cms.hhs.gov/search%23pageStart) search page.

{% hint style="info" %}
Date of birth is required to be in the\*\*`YYYYMMDD`\*\*format.
{% endhint %}

### Eligibility Response Formats

CardScan.ai provides eligibility data in **two formats** to serve different integration needs:

#### 1. **Normalized Response** (`eligibility_summarized_response`) - **Recommended**

Our clean, standardized format that normalizes data across all clearinghouses. This is what **90% of developers need** - consistent field names, standardized values, and easy-to-parse structure regardless of which clearinghouse processed the request.

#### 2. **Raw Clearinghouse Response** (`eligibility_response`) - **Advanced Use Cases**

The complete, unmodified response from the clearinghouse (Availity, Change Healthcare, etc.). Use this when you need access to clearinghouse-specific fields or want to implement your own parsing logic.

{% hint style="success" %}
**Recommendation:** Start with the `eligibility_summarized_response` for faster integration. The raw response is available when you need additional data not included in our normalized format.
{% endhint %}

{% hint style="info" %}
**Raw Response:** The complete clearinghouse response contains extensive additional data including detailed benefits, service-specific coverage, and clearinghouse-specific fields that may be useful for advanced integrations.
{% endhint %}

```json
{
  "subscriber_details": {
    "member_id": "27312103",
    "firstname": "Joshua",
    "lastname": "Marshall",
    "middlename": "Jennifer",
    "gender": "M",
    "address": {
      "address1": "391 John Spur Apt. 163",
      "city": "Lake Garrett",
      "state": "MD",
      "postalCode": "26540"
    },
    "dob": "2006-05-17"
  },
  "payer_details": {
    "payer_name": "Aetna",
    "address": {
      "address1": "PO Box 14079",
      "city": "Lexington",
      "state": "KY",
      "postalCode": "40512"
    }
  },
  "plan_details": {
    "plan_number": "2675107",
    "group_name": "Harris Ltd",
    "group_number": "137409668",
    "plan_start_date": "2022-12-01",
    "plan_eligibility_start_date": "2022-01-01",
    "plan_name": "Open Access Elect Choice",
    "plan_active": true
  },
  "coverage_summary": {
    "individual_deductible_in_network": {
      "total_amount": "$3500",
      "remaining_amount": "$2533.86"
    },
    "individual_oop_in_network": {
      "total_amount": "$6350",
      "remaining_amount": "$5383.86"
    },
    "family_deductible_in_network": {
      "total_amount": "$7000",
      "remaining_amount": "$5040.17"
    },
    "family_oop_in_network": {
      "total_amount": "$12700",
      "remaining_amount": "$10740.17"
    }
  },
  "chiropractic": {
    "co_insurance_in_network": {
      "amount": "20%"
    },
    "co_payment_in_network": {
      "amount": "$0"
    },
    "service_code": "33"
  },
  "emergency_room": {
    "co_insurance_in_network": {
      "amount": "20%"
    },
    "co_payment_in_network": {
      "amount": "$0"
    },
    "service_code": "86"
  },
  "office_visit": {
    "co_insurance_in_network": {
      "amount": "20%"
    },
    "co_payment_in_network": {
      "amount": "$0"
    },
    "service_code": "98"
  },
  "urgent_care": {
    "co_insurance_in_network": {
      "amount": "20%"
    },
    "co_payment_in_network": {
      "amount": "$0"
    },
    "service_code": "UC"
  },
  "hospital_outpatient": {
    "co_insurance_in_network": {
      "amount": "20%"
    },
    "co_payment_in_network": {
      "amount": "$0"
    },
    "service_code": "50"
  }
}
```

### Testing

To facilitate easier testing, our sandbox environment is configured to trigger specific errors based on certain input values:

* **Invalid NPI Error:** Use a provider NPI value of `12345678` to simulate an "Invalid NPI" error.
* **Mismatched DOB Error:** Use a subscriber date of birth (DOB) value of `19020402` to simulate a "Mismatched DOB" error.
* **Name Misspelled Error:** Use a subscriber first name of `Walt` and last name of `Witman` to simulate a "Name Misspelled" error.

## **Troubleshooting and Support**

Encountering issues while using the Eligibility Verification feature is not uncommon. Below are some common errors you might face, along with guidance on how to resolve them:

**InvalidNPIException**

* **Cause**: This exception occurs when the NPI (National Provider Identifier) provided is either incorrect, not found in the NPI database, or not registered with the payer.
* **Resolution**: Double-check the NPI number for accuracy. Ensure that it is correctly registered in the NPI database. You can verify NPI numbers using the [official NPI Registry.](https://npiregistry.cms.hhs.gov/)

**MismatchedDOBException**

* **Cause**: This exception is triggered when the patient's Date of Birth (DOB) does not match the records or is formatted incorrectly.
* **Resolution**: Verify that the patient's DOB is correct and formatted in the YYYYMMDD format. Cross-reference with patient records for accuracy.

**NameMisspelledException**

* **Cause**: A common issue where the patient's name might be misspelled or entered incorrectly.
* **Resolution**: Check for any common misspellings, swapping of first/last names, or typographical errors in the patient's name. Correct the name and try again.

**General Tips**

* **API Response Timeouts**: If you encounter timeouts, check your network connection and server status. If the issue persists, contact our support team.
* **Data Format Errors**: Ensure all data sent in API requests adhere to the specified format guidelines in our documentation.

### **Support Contact**

If you continue to experience issues or have specific queries, our support team is here to help:

* **Email Support**: Reach out to us at <support@cardscan.ai> with detailed information about your issue.


# Payer Matching 🔍

### Overview

Matching insurance cards to the responsible payer and identifying the necessary clearinghouse IDs for eligibility checks and claim submissions presents a complex challenge.

Insurance cards often lack explicit payer IDs, may feature only a payer's logo, present unclear payer names, or exhibit various issues that complicate accurate matching.

{% hint style="info" %}
**Note**: Payer Match is a paid feature only available to our **Enterprise customers**.
{% endhint %}

We leverage our sophisticated matching algorithms, similar to those used in our eligibility check processes, to provide detailed and accurate payer information based on submitted card data.

Matches are generated during the scanning process and will be returned once the card scan is completed.

{% hint style="warning" %}
Payer matching works best when paired with **backside scanning** as many cards have important details only found on the backside of the card.
{% endhint %}

### Payer Match

When enabled Payer Match is included in the standard [API 💻](/api#get-card) payload. When a card scan is completed, the `payer_match` object will automatically include details for canonical, clearinghouse, and custom matches (if enabled). No separate request is required.

### Canonical Match

We match card details against our internal database of payer names and identifiers. Upon finding a high-confidence match, we return the canonical payer name along with CardScan.ai's unique payer ID.

```json
{
  "card_id": "42532e8b-6d38-44fb-b680-a8de47ca4717",
  "state": "completed",
  "payer_match": {
    "cardscan_payer_id": "pay_5de0cbfe",
    "cardscan_payer_name": "United Healthcare",
    "score": "0.985"
  }
}
```

### Clearing House Matching

We also match card details with various clearinghouses, including Availity, Office Ally, Stedi, Change Healthcare and more. This feature is designed to help our customers maintain their workflows with existing vendors for processing eligibility checks and submitting claims, while significantly reducing errors.

```json
{
  "card_id": "42532e8b-6d38-44fb-b680-a8de47ca4717",
  "state": "completed",
  "payer_match": {
    "cardscan_payer_id": "pay_5de0cbfe",
    "cardscan_payer_name": "UnitedHealthcare",
    "score": "0.99",
    "matches": [
      {
        "clearinghouse": "Availity",
        "payer_id": "87726",
        "transaction_type": "professional",
        "payer_name": "UnitedHealthcare",
        "cardscan_payer_id": "pay_5de0cbfe",
        "score": "0.99",
        "metadata": {
          "last_updated": "2025-03-06T19:19:48.352506+00:00",
          "source": "2025-03-05v01"
        }
      },
      {
        "clearinghouse": "Office Ally",
        "payer_id": "87726",
        "transaction_type": "institutional",
        "payer_name": "UnitedHealthcare (UHC)",
        "cardscan_payer_id": "pay_3c226c5a",
        "score": "0.982",
        "metadata": {
          "last_updated": "2025-03-06T19:19:48.352506+00:00",
          "source": "2025-03-05v01"
        }
      }
    ]
  }
}
```

{% hint style="warning" %}
**We've moved payer matches into the `matches` key — per-payer key access is now deprecated.**
{% endhint %}

### Custom Matching

Recognizing the unique needs of our customers, we also offer the ability to match with a customer's specific payer list and return identifiers at scan time. This customization ensures that the payer information provided is directly relevant and tailored to the operational requirements of our customers.

```json
{
  "payer_match": {
    "custom": [
      {
        "custom_payer_id": "UHC",
        "custom_payer_name": "United Healthcare",
        "custom_payer_name_alt": "Alternate name for UHC",
        "score": "0.958",
        "source": "custom_payer_list_20240130"
      }
    ]
  }
}
```

### New: Configure Your Preferred Clearinghouse and Transaction Type

We now support the ability to configure your preferred clearinghouse and transaction type for payer matching and claim submissions.&#x20;

While the ability to select your preferences directly will be available in a future update, you can contact our support team today to set up your desired clearinghouse and transaction type. This ensures smoother integration with your existing workflows and more accurate processing of eligibility checks and claims.

### Access and Support

This feature is available exclusively to subscribers of our enterprise plans. For more information on accessing the Payer Match feature or for assistance in setting up and configuring custom payer matches, please contact our sales or support team.<br>


# Camera Permission Modal 📸

### Getting started

We have three distinct flows for the camera permission modal: Desktop version, Mobile version, and SDK version. Each version operates differently based on how camera permissions are requested. Below are the details for each flow:

<figure><img src="/files/Da4zTdi3HnVdNLkg8TcX" alt=""><figcaption></figcaption></figure>

This section outlines all the possibilities available when using a desktop browser. In the desktop browser flow, we need to consider web-to-mobile handoff, as this is the only version that supports this functionality.

<figure><img src="/files/cT7qYSANstOZhwvi8iC8" alt=""><figcaption></figcaption></figure>

The mobile version minimizes the flow, as it does not support web-to-mobile handoff.

<figure><img src="/files/rJtnzuPp6jOQFC5MmGlx" alt=""><figcaption></figcaption></figure>

In the SDK version, since it is a native app built from scratch, we simply inform the user if camera permission is not granted, and the user can handle the rest.

### Request Permission Modal&#x20;

If the react widget can access the browser's camera, it will automatically request camera permission without manual intervention. This only occurs if there are no strict blocks or restrictions in place on the browser.

<figure><img src="/files/N0BFxHnAuKznGKTP7HNg" alt=""><figcaption></figcaption></figure>

**requestPermissionModalConfig**

The `requestPermissionModalConfig` object allows customization of the Request Permission Modal in the application. It provides the flexibility to set custom texts for various parts of the modal, enhancing the user experience by providing clear and relevant information.

**Properties:**

* **title**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the title of the Request Permission Modal. If this property is not provided, the modal will use the default title "Waiting for camera permission"
* **text**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the main content text of the Request Permission Modal. This text instruct that you must accept to turn on the camera. If this property is not provided, the modal will use the default text "Click Allow to grant camera permission."

### Help Modal&#x20;

When the react widget is unable to access the browser camera feed it will prompt the user to provide the necessary browser permissions. It displays a widget with a browser-specific video demonstrating how to grant permission to the camera.

<figure><img src="/files/2VhD7V33QTEQclKZh1Lc" alt=""><figcaption></figcaption></figure>

**helpModalConfig**

The `helpModalConfig` object allows customization of the Help Modal in the application. It provides the flexibility to set custom texts for various parts of the modal, enhancing the user experience by providing clear and relevant information.

For the instructional video, there are six different versions tailored for each browser on both desktop and mobile:

* Google Chrome (desktop version)
* Google Chrome (mobile app version)
* Safari (desktop version)
* Safari (mobile app version)
* Firefox (desktop version)
* Firefox (mobile version)
* Edge (desktop version)

Depending on the browser in which the SDK is opened, it will automatically detect and display the appropriate instructional video. If the user is on an unsupported browser, the Google Chrome video will be shown by default.

**Properties:**

* **title**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the title of the Help Modal. If this property is not provided, the modal will use the default title "Setup camera"
* **text**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the main content text of the Help Modal. This text explains the reason why camera permission is needed. If this property is not provided, the modal will use the default text "To access, you need to grant permission to use the device's camera. If the prompt hasn't appeared, try reloading the page."
* **instructionTexts**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the instruction text of the Help Modal. This provides users with clear instructions on how to enable their cameras. If this property is not provided, the modal will use the default text "If the prompt does not appear on browser, follow this path Go to Settings > Privacy and Security > Camera. Then enable camera access and reload the page."
* **tryAgainText**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the text of the try again button. This button will appear if the browser allows requesting camera permission without needing to navigate into the device settings. If this property is not provided, the modal will use the default text "Try again."

### Camera Permission Modal&#x20;

If the user declines camera permissions in the Request Permission modal, they will be redirected to the Camera Permission modal, with the 3 options available:

1. **Retry Camera Access**: Clicking this button will attempt to re-enable camera access and open the camera. In some cases, this button may not appear if the device restricts camera permissions for the browser.
2. **Continue on Mobile Phone**: If the user has a web-to-mobile handoff feature enabled, a button with a timer will be displayed. Once the timer expires, the Web-to-Mobile Handoff modal will appear. This option will not be available if the widget is opened on a mobile device.
3. **Get Help**: Clicking this option will redirect the user to the Help Modal.

<figure><img src="/files/d30rfBbn3SVyudEj7USa" alt=""><figcaption></figcaption></figure>

**cameraPermissionModalConfig**

The `cameraPermissionModalConfig` object allows customization of the Help Modal in the application. It provides the flexibility to set custom texts for various parts of the modal, enhancing the user experience by providing clear and relevant information.

**Properties:**

* **enabled**:
  * **Type**: `boolean` (Optional)
  * **Description**: A boolean flag to enable or disable the Camera Permission Modal and Help Modal . If this property is not provided, the modal will default to being enabled (`true`).
* **title**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the title of the Camera Permission Modal. If this property is not provided, the modal will use the default title "Camera permission needed"
* **text**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the main content text of the Camera Permission Modal. This text explains the reason why camera permission is needed. If this property is not provided, the modal will use the default text "This application needs access to the camera to scan medical insurance card".
* **retryText**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the text for retry button.  If this property is not provided, the button will use the default text "Retry camera access"
* **webToMobileText**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the text for web to mobile button.  If this property is not provided, the button will use the default text "Continue on Mobile Phone"
* **helpText**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the text for help button.  If this property is not provided, the button will use the default text "Get help"

### Web to Mobile Handoff Modal&#x20;

If the modal is enabled, it will appear as an option, providing users with an alternative way to scan using their mobile device in case their desktop lacks a camera.

1. **Scanning QR**: If users scan the QR it will redirect them to the widget mobile version to continue the scan from that device.

<figure><img src="/files/52E85xFbiBrwmbJvthYz" alt=""><figcaption></figcaption></figure>

**webToMobileHandOffConfig**

The `webToMobileHandOffConfig` object allows customization of the Web to Mobile Handoff Modal in the application. It provides the flexibility to set custom texts for various parts of the modal, enhancing the user experience by providing clear and relevant information.

For implementing the functionality is on the next page:

{% content-ref url="/spaces/-MYkGp0C8rvjnJYLAI\_u/pages/7fxxgDKsQzcSkAmd0ySk" %}
[Web to Mobile Handoff 📲](/advanced-features/web-to-mobile-handoff)
{% endcontent-ref %}

**Properties:**

* **enabled**:
  * **Type**: `boolean`  or `string` (Optional)
  * **Description**: A boolean flag to enable or disable the web to mobile hand off modal. If this property is not provided, the modal default to being enabled (`false`).
    * If you input "`always"` this option will not trigger the camera permission modal. Instead, it will directly display the web-to-mobile modal without prompting for camera permissions.
  * **Example:**
    * **true**: Displays the web-to-mobile handoff option if the camera permission fails.
    * **false**: Does not display the web-to-mobile handoff in any case.
    * **"always"**: Automatically shows the web-to-mobile handoff without trying the camera permission flow.
* **urlBase**:
  * **Type**: `string` (Optional)
  * **Description**: Define the url that will be redirected when users scan the QR code.
* **companyName**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the title of the Web to mobil handoff Modal. If this property is not provided, the modal will use the default title "Use your phone to scan your card"
* **autoRedirect**:
  * **Type**: `int` (Optional)
  * **Description**: Sets the time in seconds for redirecting the user for web to mobile handoff on camera permission modal.  If this property is not provided, the default seconds will be 15.
* **title**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the title of the Web to mobil handoff Modal. If this property is not provided, the modal will use the default title "Use your phone to scan your card"
* **instructionsText**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the text for instructions of how to use web to mobile handoff.  If this property is not provided, the button will use the default text "Scan the QR code with your phone\nFollow the link to securely scan your card.\nReturn here when you're finished."
* **retryButtonText**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the text for retry button.  If this property is not provided, the button will use the default text "Try again"

### No Camera Modal

This modal is applicable only for mobile SDKs (React Native, Flutter, iOS, or Android) and indicates that the app does not have camera permission. When you click on go to settings it will redirect to the device configuration to enable the camera access.

<figure><img src="/files/axrKbPPUWzM5bD8RrSBl" alt=""><figcaption></figcaption></figure>

**noCameraModalConfig**

The `noCameraModalConfig` object allows customization of the No Camera Modal in the application. It provides the flexibility to set custom texts for various parts of the modal, enhancing the user experience by providing clear and relevant information.

**Properties:**

* **title**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the title of the No Camera Modal. If this property is not provided, the modal will use the default title "No camera access"
* **instructions**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the main content text of the No Camera Modal. This text explains where you need to navigate to enable the camera. If this property is not provided, the modal will use the default text "To scan your card, allow us to use your camera in Settings > Privacy > Camera"
* **settingsButton**:
  * **Type**: `string` (Optional)
  * **Description**: Sets the text for settings button.  If this property is not provided, the button will use the default text "Go to settings"


# Web to Mobile Handoff 📲

<figure><img src="/files/oQ2VleJjU3W8SGVUqz1n" alt=""><figcaption><p>Web to Mobile - Handoff</p></figcaption></figure>

## Overview

Many users encounter difficulties in enabling their webcams on desktop browsers, leading to a frustrating experience during the scanning process. Additionally, most laptop and desktop webcams struggle with low light settings and card movement, further impacting the success rate of scans.

To address these challenges, we have introduced the "Handoff" feature in our React widget. This feature allows users to seamlessly switch to their mobile devices to complete the scanning process when faced with difficulties or failures on the desktop.

## Handoff

The Handoff feature is designed to be triggered under specific conditions such as **setup failure**, **camera failure**, or after **two unsuccessful scanning attempts**. When activated, the feature generates a magic link and displays a QR code that users can scan with their mobile devices to continue the process.

A standalone endpoint is responsible for validating the magic link and providing the user with the necessary instructions on their mobile device. Once the scanning is successfully completed on the mobile device, the user is instructed to return to the desktop browser. The desktop widget then updates automatically to reflect the success and returns the results locally.<br>

{% hint style="info" %}
**Note**: Handoff works seamlessly with our **Eligibility** and **Payer Match** features.
{% endhint %}

## Configuration

To enable and configure the Handoff feature in your React widget, use the `webToMobileHandoffConfig` prop:

```typescript
<CardScanView 
  sessionToken={token}
  onSuccess={cardScanSuccess}
  onCancel={cardScanCancel}
  onError={cardScanError}
  webToMobileHandoffConfig={{
    enabled: true,
    companyName: "HealthHaven",
    modalTitle: "Scanning Failed",
    modalText: "Please scan this QR Code to continue scanning on your mobile phone.\nReturn here once successful to continue.",
    buttonText: "Try again"
  }}
/>
```

* `enabled`: Set to `true` to enable the Handoff feature.
* `companyName`: (Optional) Specify the name of your company to be displayed to the patient during the handoff process.
* `modalTitle`: (Optional) Provide a custom title for the modal displayed during the handoff process. Defaults to `"Scanning Failed"`.
* `modalText`: (Optional) Provide custom text for the modal body. This text guides the user on what to do next. Defaults to `"Please scan this QR Code to continue scanning on your mobile phone.\nReturn here once successful to continue.".`
* `buttonText`: (Optional) Provide custom text for the button displayed in the modal. Defaults to `"Try again".`

### QR Code and URL

The QR code displayed to the user will be comprised of a URL that looks like `https://capture.cardscan.ai/?token=<short-lived-token>`. This site guides the user through the scanning process and, once completed, instructs the user to continue on their desktop.

### Self-hosting

Alternative the `urlBase` prop can be used to generate a QR code that will direct the user to the provided URL, allowing for a customized scanning experience on your own domain.

```
webToMobileHandoffConfig={{
  enabled: true,
  urlBase: "https://capture.yourURL.com",
}}
```

* `enabled`: Set to `true` to enable the Handoff feature.
* `urlBase`: (Optional) Specify the base URL to direct the user to a page on your mobile-friendly web app or a standalone site for the scanning process. This allows for complete customization of the landing page and keeps the user within your company's domains.

#### Developer Responsibilities

When opting for self-hosting, the developer is responsible for the following:

1. **Providing Instructions to the Mobile User:** The landing page should guide the user through the scanning process on their mobile device.
2. **Exchanging the Short-lived Token for a JWT:** The mobile app or web page should handle the exchange of the short-lived token provided in the QR code for a JWT (JSON Web Token) to authenticate the scanning session. Below are the steps to implement this process:
   1. The React component will [generate a magic link token](/api#generate-magic-link) and send it to the specified `urlBase` as a query parameter.
   2. When the app hosted at `urlBase` loads, it should call the [validation endpoint](/api#validate-magic-link), which will return an [access token](/api#get-access-token) (JWT) upon successful validation.
   3. Once the JWT is obtained from the validation endpoint, pass it to the React component to initiate the scanning session.
3. **Loading the CardScanView with the Token:** The mobile app or web page should load the `CardScanView` component with the obtained JWT to initiate the scanning process.
4. **Instructing the User to Return to the Desktop:** Upon successful completion of the scanning process on the mobile device, the user should be instructed to return to their desktop browser to continue their journey or view the results.

## Demo

To provide a clearer understanding of the Handoff feature in action, we have created a video demo that showcases the seamless transition from desktop to mobile scanning.

{% embed url="<https://www.youtube.com/embed/tFUkeYtoNAY?si=NCgW7XNx4nKsV2N6>" %}
Handoff Demo
{% endembed %}


# Webhooks 🔔

Webhooks provide real-time notifications when events occur in your CardScan.ai account, enabling you to build responsive applications that react immediately to card scanning and eligibility verification events.

## Introduction

Webhooks are how services notify each other of events. At their core, they are just a POST request to a pre-determined endpoint that you control.

The endpoint can be whatever you want, and you can configure them from the [CardScan Dashboard](https://dashboard.cardscan.ai). You normally use one endpoint per service, and that endpoint listens to all of the event types you're interested in.

For example, if you receive webhooks from CardScan.ai, you can structure your URL like: `https://www.example.com/cardscan/webhooks/`.

The way to indicate that a webhook has been processed successfully is by returning a 2xx (status code 200-299) response to the webhook message within a reasonable time-frame (15 seconds).

{% hint style="warning" %}
It's important to disable CSRF protection for your webhook endpoint if your framework enables it by default.
{% endhint %}

Another critical aspect of handling webhooks is to verify the signature and timestamp when processing them. You can learn more about this in the [signature verification](#signature-verification) section.

## Webhook Payload Structure

{% hint style="info" %}
**Security & Privacy:** Webhook payloads contain only event metadata and identifiers. Detailed card information and eligibility results are **not** included in webhook payloads for security reasons. You'll need to use the [API](/api) to fetch full details using the provided IDs.
{% endhint %}

All webhook events follow a consistent structure:

```json
{
  "type": "card.completed",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "configuration": {
    "enable_backside_scan": false,
    "enable_livescan": false,
    "enable_payer_match": true
  },
  "created_at": "2025-07-02 06:40:39.522710+00:00",
  "deleted": false,
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 06:41:06.845194+00:00",
  "user_id": "postman_hotpath"
}
```

## Available Events

CardScan.ai sends webhooks for card scanning and eligibility verification events. Each webhook includes event metadata, but you'll need to call our API to get the full details.

### Card Events

#### card.created

Triggered when a new insurance card is created at the start of a scanning attempt.

```json
{
  "type": "card.created",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "configuration": {
    "enable_backside_scan": false,
    "enable_livescan": false,
    "enable_payer_match": true
  },
  "created_at": "2025-07-02 06:40:39.522710+00:00",
  "deleted": false,
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 06:40:39.522710+00:00",
  "user_id": "postman_hotpath"
}
```

#### card.completed

Triggered after a successful insurance card scan. Use the `card_id` to fetch the extracted data via the [Get Card API](/api#get-card).

```json
{
  "type": "card.completed",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "configuration": {
    "enable_backside_scan": false,
    "enable_livescan": false,
    "enable_payer_match": true
  },
  "created_at": "2025-07-02 06:40:39.522710+00:00",
  "deleted": false,
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 06:41:06.845194+00:00",
  "user_id": "postman_hotpath"
}
```

#### card.error

Triggered when an error occurs during an insurance card scan.

```json
{
  "type": "card.error",
  "card_id": "b6d83d59-9577-4cff-9df2-b6b667259817",
  "configuration": {
    "enable_backside_scan": false,
    "enable_livescan": false,
    "enable_payer_match": true
  },
  "created_at": "2025-07-02 03:45:22.479359+00:00",
  "deleted": false,
  "error": {
    "code": "UnknownError",
    "message": "Unknown processing error, please try again. If the problem persists, please contact support.",
    "type": "UnknownError"
  },
  "session_id": "ses_5952a878-c58d-4eb0-af55-2ca35aee6bd3",
  "updated_at": "2025-07-02 03:45:37.080034+00:00",
  "user_id": "postman_hotpath"
}
```

#### card.deleted

Triggered when a scanned insurance card is marked as deleted.

```json
{
  "type": "card.deleted",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "configuration": {
    "enable_backside_scan": false,
    "enable_livescan": false,
    "enable_payer_match": true
  },
  "created_at": "2025-07-02 06:40:39.522710+00:00",
  "deleted": true,
  "deleted_at": "2025-07-02 16:30:14.856792+00:00",
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 16:30:14.856792+00:00",
  "user_id": "postman_hotpath"
}
```

### Eligibility Events

{% hint style="info" %}
Eligibility webhooks require the [Eligibility Verification](/advanced-features/eligibility-verification) feature to be enabled on your account.
{% endhint %}

#### eligibility.created

Triggered when a new eligibility record is created for an insurance card.

```json
{
  "type": "eligibility.created",
  "eligibility_id": "elig_12345",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "state": "pending",
  "created_at": "2025-07-02 06:45:14.856792+00:00",
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 06:45:14.856792+00:00",
  "user_id": "postman_hotpath"
}
```

#### eligibility.completed

Triggered when an eligibility check for an insurance card is successfully completed. Use the `eligibility_id` to fetch the full eligibility details via the API.

```json
{
  "type": "eligibility.completed",
  "eligibility_id": "elig_12345",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "state": "completed",
  "created_at": "2025-07-02 06:45:14.856792+00:00",
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 06:47:21.123456+00:00",
  "user_id": "postman_hotpath"
}
```

#### eligibility.error

Triggered when an error occurs during an eligibility check.

```json
{
  "type": "eligibility.error",
  "eligibility_id": "elig_12345",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "state": "error",
  "created_at": "2025-07-02 06:45:14.856792+00:00",
  "error": {
    "code": "PayerNotSupported",
    "message": "Unable to verify eligibility with payer",
    "type": "PayerNotSupported"
  },
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 06:47:30.789012+00:00",
  "user_id": "postman_hotpath"
}
```

#### eligibility.deleted

Triggered when an eligibility record is deleted.

```json
{
  "type": "eligibility.deleted",
  "eligibility_id": "elig_12345",
  "card_id": "c906f145-7ca4-4117-a6f0-9ab5323e5423",
  "created_at": "2025-07-02 06:45:14.856792+00:00",
  "deleted": true,
  "deleted_at": "2025-07-02 16:30:14.856792+00:00",
  "session_id": "ses_36a1afca-4f87-4d77-a24d-559d63486e1b",
  "updated_at": "2025-07-02 16:30:14.856792+00:00",
  "user_id": "postman_hotpath"
}
```

## Processing Webhook Events

Since webhook payloads only contain event metadata, you'll typically follow this pattern:

### Example: Processing a Card Completion

{% tabs %}
{% tab title="Node.js" %}

```javascript
import { CardScanApi } from '@cardscan.ai/cardscan-client';

app.post('/webhooks/cardscan', async (req, res) => {
  try {
    // Verify the webhook signature first
    const payload = wh.verify(req.body, req.headers);
    
    // Acknowledge the webhook immediately
    res.status(200).send('OK');
    
    // Process the event asynchronously
    if (payload.type === 'card.completed') {
      const { card_id, user_id } = payload;
      
      // Fetch the full card details using the API
      const client = new CardScanApi({
        sessionToken: await getSessionTokenForUser(user_id),
        live: true
      });
      
      const cardDetails = await client.getCard(card_id);
      
      // Process the card data
      await processCompletedCard(cardDetails);
    }
  } catch (err) {
    console.error('Webhook processing failed:', err);
    res.status(400).send('Invalid signature');
  }
});
```

{% endtab %}

{% tab title="Python" %}

```python
import requests
from cardscan_client import CardScanApi

@app.route('/webhooks/cardscan', methods=['POST'])
def handle_webhook():
    try:
        # Verify the webhook signature first
        payload = wh.verify(request.data, dict(request.headers))
        
        # Acknowledge the webhook immediately
        response = make_response('OK', 200)
        
        # Process the event asynchronously
        if payload['type'] == 'card.completed':
            card_id = payload['card_id']
            user_id = payload['user_id']
            
            # Fetch the full card details using the API
            session_token = get_session_token_for_user(user_id)
            client = CardScanApi(session_token=session_token, live=True)
            
            card_details = client.get_card(card_id)
            
            # Process the card data
            process_completed_card(card_details)
            
        return response
    except Exception as e:
        print(f"Webhook processing failed: {e}")
        return make_response('Invalid signature', 400)
```

{% endtab %}
{% endtabs %}

## API Client Libraries

To make processing webhook events easier, use our official API client libraries that include typed models for parsing responses:

{% embed url="<https://github.com/CardScan-ai/api-clients/tree/main>" %}

Available clients:

* **TypeScript/JavaScript** - `@cardscan.ai/cardscan-client`
* **Python** - `cardscan-client`
* **Swift** - Swift Package Manager
* **Kotlin** - `com.cardscan:api`
* **Dart** - `cardscan_client`

These clients provide strongly-typed models for card details, eligibility results, and API responses, making it easier to work with the data you fetch after receiving webhook notifications.

## Adding Webhook Endpoints

To start receiving webhook notifications, you need to configure your endpoints in the CardScan Dashboard.

### Dashboard Configuration

1. Log into your [CardScan Dashboard](https://dashboard.cardscan.ai)
2. Navigate to the **Webhooks** section
3. Click **Add Endpoint**
4. Enter your webhook URL (e.g., `https://your-domain.com/webhooks/cardscan`)
5. Select the event types you want to receive
6. Click **Create Endpoint**

{% hint style="info" %}
If you don't specify any event types, your endpoint will receive all events by default. We recommend selecting specific event types to avoid receiving unnecessary messages.
{% endhint %}

{% hint style="warning" %}
**API Configuration Coming Soon:** While webhook endpoints are currently managed through the dashboard, API-based endpoint management is available upon request. Contact <team@cardscan.ai> if you need programmatic webhook management.
{% endhint %}

## Testing Webhooks

Once you've added an endpoint, you should test it to ensure it's working correctly.

### Dashboard Testing

1. Go to your webhook endpoint in the dashboard
2. Click on the **Testing** tab
3. Select an event type to test
4. Click **Send Test Event**
5. Review the response and check your endpoint logs

After sending a test event, you can click into the message to view:

* The complete message payload
* All delivery attempts
* Success/failure status
* Response details

### Test Event Payloads

Test events use the same structure as real events but with clearly marked test data:

```json
{
  "type": "card.completed",
  "card_id": "test_card_12345",
  "configuration": {
    "enable_backside_scan": false,
    "enable_livescan": false,
    "enable_payer_match": true
  },
  "created_at": "2025-07-02 15:50:14.856792+00:00",
  "deleted": false,
  "session_id": "test_session",
  "updated_at": "2025-07-02 15:50:14.856792+00:00",
  "user_id": "test_user"
}
```

## Signature Verification

Webhook signatures let you verify that webhook messages are actually sent by CardScan.ai and not a malicious actor. **You should always verify webhook signatures in production.**

### Why Verify Signatures?

Without signature verification, any malicious actor could send fake webhook events to your endpoint, potentially compromising your application's security and data integrity.

### How to Verify

CardScan.ai uses [Svix](https://svix.com) for webhook delivery, which provides battle-tested signature verification. You can use Svix's libraries to easily verify webhook signatures:

{% tabs %}
{% tab title="Node.js" %}

```javascript
import { Webhook } from "svix";

const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";

// These headers are sent with every webhook
const headers = {
  "svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
  "svix-timestamp": "1614265330",
  "svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
};
const payload = '{"type": "card.completed", "data": {...}}';

const wh = new Webhook(secret);
try {
  // Throws on error, returns the verified content on success
  const verifiedPayload = wh.verify(payload, headers);
  console.log("Webhook verified successfully:", verifiedPayload);
} catch (err) {
  console.error("Webhook verification failed:", err.message);
}
```

{% endtab %}

{% tab title="Python" %}

```python
from svix.webhooks import Webhook

secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"

headers = {
    "svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
    "svix-timestamp": "1614265330",
    "svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
}
payload = '{"type": "card.completed", "data": {...}}'

wh = Webhook(secret)
try:
    # Throws on error, returns the verified content on success
    verified_payload = wh.verify(payload, headers)
    print("Webhook verified successfully:", verified_payload)
except Exception as e:
    print("Webhook verification failed:", str(e))
```

{% endtab %}

{% tab title="Go" %}

```go
package main

import (
    "fmt"
    "github.com/svix/svix-webhooks/go"
)

func main() {
    secret := "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
    
    headers := map[string]string{
        "svix-id":        "msg_p5jXN8AQM9LWM0D4loKWxJek",
        "svix-timestamp": "1614265330",
        "svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
    }
    payload := `{"type": "card.completed", "data": {...}}`
    
    wh, err := svix.NewWebhook(secret)
    if err != nil {
        panic(err)
    }
    
    verifiedPayload, err := wh.Verify(payload, headers)
    if err != nil {
        fmt.Printf("Webhook verification failed: %v\n", err)
        return
    }
    
    fmt.Printf("Webhook verified successfully: %s\n", verifiedPayload)
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
For more examples and supported languages, check out the [Svix webhook verification documentation](https://docs.svix.com/receiving/verifying-payloads/how).
{% endhint %}

### Finding Your Webhook Secret

Each webhook endpoint has a unique secret key that you can find in the CardScan Dashboard:

1. Go to your webhook endpoint settings
2. Click **Signing Secret**
3. Copy the secret (it starts with `whsec_`)

## Retry Schedule

CardScan.ai automatically retries failed webhook deliveries using an exponential backoff strategy to ensure reliable delivery.

### Retry Attempts

Each webhook message is attempted based on the following schedule:

* **Immediately**
* **5 seconds**
* **5 minutes**
* **30 minutes**
* **2 hours**
* **5 hours**
* **10 hours**
* **10 hours** (final attempt)

For example, a webhook that fails three times before succeeding will be delivered approximately 35 minutes and 5 seconds after the first attempt.

### Automatic Disabling

If all delivery attempts to an endpoint fail for a period of **5 days**, the endpoint will be automatically disabled to prevent unnecessary retry attempts.

### Manual Retries

You can manually retry webhook deliveries from the dashboard:

**Single Message Retry:**

1. Find the failed message in your webhook endpoint logs
2. Click the options menu next to the failed attempt
3. Select **Resend** to retry the delivery

**Bulk Recovery:**

1. Go to your endpoint's details page
2. Click **Options > Recover Failed Messages**
3. Choose a time window to recover from
4. All failed messages in that period will be resent

## Troubleshooting

Here are common issues and solutions when working with CardScan.ai webhooks:

### Common Problems

#### Not Using Raw Payload Body

**Most common issue.** When verifying signatures, you must use the raw string body of the webhook payload exactly as received. Don't parse it as JSON first.

```javascript
// ❌ Wrong - don't parse JSON first
const parsedBody = JSON.parse(requestBody);
const stringifiedBody = JSON.stringify(parsedBody);
wh.verify(stringifiedBody, headers); // This will fail

// ✅ Correct - use raw body
wh.verify(requestBody, headers);
```

#### Wrong Secret Key

Make sure you're using the correct secret for your specific endpoint. Each endpoint has its own unique secret key.

#### Incorrect Response Codes

Return 2xx status codes (200-299) for successful webhook processing, even if your business logic determines the event should be ignored.

```javascript
// ✅ Correct
app.post('/webhooks/cardscan', (req, res) => {
  try {
    const payload = wh.verify(req.body, req.headers);
    // Process the webhook...
    res.status(200).send('OK');
  } catch (err) {
    res.status(400).send('Invalid signature');
  }
});
```

#### Response Timeouts

Webhooks must respond within **15 seconds**. For complex processing, acknowledge the webhook immediately and process asynchronously:

```javascript
app.post('/webhooks/cardscan', async (req, res) => {
  try {
    const payload = wh.verify(req.body, req.headers);
    
    // Acknowledge immediately
    res.status(200).send('OK');
    
    // Process asynchronously
    processWebhookAsync(payload);
  } catch (err) {
    res.status(400).send('Invalid signature');
  }
});
```

### Failure Recovery

#### Re-enable a Disabled Endpoint

If your endpoint was disabled due to consecutive failures:

1. Fix the underlying issue with your endpoint
2. Go to the webhook dashboard
3. Find your endpoint and click **Enable Endpoint**

#### Replay Failed Messages

To recover from downtime or misconfigurations:

1. **Single Event:** Find the message and click **Resend**
2. **Time Range:** Use **Options > Recover Failed Messages** to replay all failed events from a specific time period
3. **From Specific Message:** Click the options menu on any message and select **Replay all failed messages since this time**

{% hint style="info" %}
Need help with your webhook implementation? Contact us at <team@cardscan.ai> and we'll help you get set up correctly.
{% endhint %}


# Overseer Rules Engine ⚙️

## Overview

**Overseer** is a flexible, dynamic rules engine designed to process, validate, and enforce business rules around insurance card data. Currently production-ready and used internally for card validation and eligibility processing, Overseer is now available to select clients.

By tailoring rules to the specific needs of each account, Overseer ensures accuracy, efficiency, and adaptability for a variety of use cases, particularly in eligibility requests.

## Current Availability

Overseer is available as an add-on feature for Enterprise accounts. Contact the CardScan team to discuss pricing, rule creation, testing, and deployment for your specific use cases.

## Simple Rule Examples

Here are examples of simple rules that can be defined within Overseer:

### Post-Card-Scanning Example

```json
{
  "id": "rule_001",
  "name": "Block Medicare cards from eligibility workflow",
  "trigger": "post_card_scanning",
  "conditions": {
    "any": [
      { "field": "card.payer_name", "comparison": "contains", "value": "Medicare" },
      { "field": "card.payer_name", "comparison": "contains", "value": "CMS" }
    ]
  },
  "actions": [
    {
      "type": "block_request",
      "message": "Medicare eligibility verification not supported via this workflow. Please use dedicated Medicare portal."
    }
  ],
  "description": "Block Medicare cards from continuing to eligibility verification."
}
```

### Pre-Eligibility Example

```json
{
  "id": "rule_002",
  "name": "Modify member ID for XYZ Insurance",
  "trigger": "pre_eligibility_request",
  "conditions": {
    "all": [
      { "field": "card.payer_name", "comparison": "equals", "value": "XYZ Insurance" },
      { "field": "card.member_number", "comparison": "startsWith", "value": "128" }
    ]
  },
  "actions": [
    {
      "type": "prepend_value",
      "field": "eligibility_request.subscriber.member_id",
      "set_value": "000"
    }
  ],
  "description": "If payer is XYZ Insurance and member ID starts with '128', prepend '000' to member ID."
}
```

## Capabilities

1. **Dynamic Rule Management**
   * Rules are loaded on a per-account basis, allowing for customized processing tailored to client-specific requirements.
   * Rules can be updated without disrupting existing workflows.
2. **Flexible Rule Evaluation**
   * Supports a wide range of conditions, including validations, checks, and thresholds.
   * Rules can be simple (e.g., verifying required fields) or more advanced (e.g., cross-checking values with approved lists, calling external APIs).
3. **Clear and Actionable Outcomes**
   * Each rule can modify the underlying payload or provide a clear outcome, such as approve, reject, or flag for further review.
   * If a rule adjusts the payload, Overseer can optionally re-run affected rules to ensure the changes are fully evaluated.
   * Rules are evaluated sequentially, ensuring transparency in decision-making.
4. **Scalable Processing**
   * Overseer can handle high volumes of data efficiently, ensuring timely and reliable results for large-scale operations.

## Common Use Cases

Overseer is designed to address a variety of scenarios, particularly for pre-eligibility workflows:

* **Validate Card Data**: Ensure extracted card information meets quality standards and required field completeness.
* **Enrich Card Data**: Add missing information or standardize data formats using external APIs or internal databases.
* **Route Based on Payer**: Direct different payers through specific workflows or block unsupported payers.
* **Validate Member Details Before RTE**: Ensure all required member information is accurate and complete before real-time eligibility requests.
* **Adjust Payload Before RTE Request**: Modify or enrich request payload data to align with payer-specific requirements.
* **Redirect RTE Requests**: Dynamically modify the routing of real-time eligibility (RTE) requests based on specific rules, such as payer identification, regional preferences, or account-specific routing logic.
* **Custom Payer Match**: Match card details against a custom-defined lists of approved or prioritized payers.
* **Match Patients Against Employer or MPI**: Cross-reference patient data with employer records or a centralized master patient index (via internal DB or API).
* **Additional Custom Rules**: Support account-specific rules that align with unique business workflows.

{% hint style="info" %}
💡 APIs for adding or modifying Overseer rules will be released in Q4. Before then the CardScan team will be happy to help create, test and deploy rules.
{% endhint %}

## Rule Structure & Capabilities

### 1️⃣ Triggers

Rules can be executed at different points in the card scanning and eligibility request lifecycle, with access to different data contexts:

| Trigger                          | Description                                                                                                        | Available Data                                         |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ |
| **`post_card_scanning`**         | Runs **after** card scanning is complete. Used for validating, enriching, or routing based on extracted card data. | Card data only                                         |
| **`pre_eligibility_request`**    | Runs **before** eligibility is checked. Used for modifying or validating the request payload.                      | Card data + Eligibility request object                 |
| **`post_eligibility_request`**   | Runs **after** eligibility is checked. Used for modifying or analyzing the response.                               | Card data + Eligibility request + Eligibility response |
| **`failed_eligibility_request`** | Runs when an **eligibility request fails** due to an error or rejection.                                           | Card data + Eligibility request + Error details        |

***

### 2️⃣ Conditions

Conditions define when a rule is applied. They support **AND** and **OR** logic:

| Condition Type | Description                                                                             |
| -------------- | --------------------------------------------------------------------------------------- |
| **`all`**      | **AND logic** – All conditions in this group must be true for the rule to apply.        |
| **`any`**      | **OR logic** – At least one condition in this group must be true for the rule to apply. |

***

## Operators

| Operator          | Description                                                                                                                 |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **`equals`**      | Exact match comparison. Example: `"card.payer_name" equals "XYZ Insurance"`                                                 |
| **`startsWith`**  | Checks if a string starts with a specific value. Example: `"card.member_number" starts with "128"`                          |
| **`dateBefore`**  | Checks if a date field is in the past. Example: `"eligibility_request.subscriber.date_of_birth" dateBefore "today"`         |
| **`dateAfter`**   | Checks if a date field is in the future. Example: `"eligibility_request.subscriber.effective_date" dateAfter "today"`       |
| **`contains`**    | Checks if a value exists within an array or string. Example: `"eligibility_response.services" contains "authorized"`        |
| **`greaterThan`** | Checks if a numerical field is greater than a given value. Example: `"eligibility_response.copay" greaterThan 25`           |
| **`lessThan`**    | Checks if a numerical field is less than a given value. Example: `"eligibility_response.deductible.remaining" lessThan 500` |

## Available Data Context

The data available to rules depends on the trigger point:

### Post-Card-Scanning Context

```json
{
  "card": {
    "card_id": "uuid",
    "member_number": "12345",
    "payer_name": "XYZ Insurance",
    "member_name": "John Doe",
    "group_number": "GRP001",
    "plan_name": "Choice Plus",
    "rx_bin": "610279",
    "rx_pcn": "9987"
  }
}
```

### Pre-Eligibility Context

```json
{
  "card": {
    "card_id": "uuid",
    "member_number": "12345",
    "payer_name": "XYZ Insurance",
    "member_name": "John Doe",
    "group_number": "GRP001"
  },
  "eligibility_request": {
    "subscriber": {
      "first_name": "John",
      "last_name": "Doe",
      "date_of_birth": "19900101",
      "member_id": "12345"
    },
    "provider": {
      "npi": "1234567890",
      "first_name": "Jane",
      "last_name": "Smith"
    }
  }
}
```

### Post-Eligibility Context (includes all above plus)

```json
{
  "eligibility_response": {
    "services": ["authorized"],
    "copay": 25.00,
    "deductible": {
      "remaining": 500.00
    },
    "coverage": {
      "active": true,
      "effective_date": "2024-01-01"
    }
  }
}
```

### 3️⃣ Actions List

Actions define how a request or response is modified:

| Action Type              | Description                                               |
| ------------------------ | --------------------------------------------------------- |
| `modify_request`         | Modifies a request field.                                 |
| `block_request`          | Blocks the request and returns a reason.                  |
| `return_custom_response` | Modifies the response and sets a custom message.          |
| `prepend_value`          | Pre-pends a string to a field (e.g., `"000" + memberId`). |
| `append_value`           | Appends a string to a field.                              |
| `replace_value`          | Replaces a field value with a new value.                  |
| `log`                    | Logs a message for debugging.                             |
| `set_flag`               | Adds a flag to the request or response.                   |
| `send_webhook`           | Sends a webhook notification (Coming Soon!)               |

## Additional Parameters

* `name` - A human friendly display name for the rule.
* `id` - Unique identifier for the rule.
* `description` - A helpful description for understanding a rules purpose and for debugging, not used in processing.

## Rule Execution Order & Dependencies

Overseer evaluates rules sequentially, ensuring deterministic processing. If a rule modifies a request or response, any dependent rules will re-evaluate as needed. Rules can be structured to minimize unintended overrides.

## Post-Eligibility Rules for Modifying Responses

Post-eligibility rules allow **dynamic modifications** to eligibility responses before they are finalized. Rules can:

* **Flag responses with metadata** (e.g., `"pre_approved": true` if services are authorized).
* **Modify the return message** based on conditions.
* **Override or enhance eligibility decisions dynamically**.

**Example Use Case:**

* An eligibility check returns **"authorized"** services.
* The rule **sets a flag** (`pre_approved: true`) for downstream processing.
* The final response message is **overwritten** to reflect service authorization.

```json
{
  "id": "rule_008",
  "name": "Modify response for authorized services",
  "trigger": "post_eligibility_request",
  "conditions": {
    "all": [
      { "field": "eligibility_response.services", "comparison": "contains", "value": "authorized" }
    ]
  },
  "actions": [
    {
      "type": "set_flag",
      "field": "eligibility_response.metadata.pre_approved",
      "value": true
    },
    {
      "type": "return_custom_response",
      "message": "Services authorized. Eligibility confirmed."
    }
  ],
  "description": "If the eligibility response contains 'authorized' services, mark as pre-approved and update response."
}
```

## Webhook-Driven Preprocessing for Eligibility Rules

Our eligibility rules support **pre-fetching external data** before processing, allowing customers to **integrate their own APIs** for additional checks. This ensures eligibility decisions can incorporate **real-time approvals, employer verification, or other business logic** without modifying the request manually.

Before an eligibility request is processed, a **customer-configured webhook** is called. The webhook response is then **injected into the request context**, allowing rules to evaluate the additional data just like any other condition.

### Key Benefits

✅ **Flexible API Integration** → Use your own API to check patient pre-approval, employer verification, or other conditions.

✅ **Secure Credential Management** → API keys are stored in **AWS Secrets Manager** and injected dynamically.

✅ **Seamless Rule Execution** → Webhook responses are **mapped into conditions** without additional processing logic.

✅ **Configurable Timeouts & Failover** → Define webhook timeouts to ensure smooth operation even if the external API is slow.

### Example Use Case

If a **patient is already pre-approved**, the rule can **skip the eligibility request** and return a response immediately:

* The webhook is called with `{ "memberId": "12345" }`.
* The API returns `{ "patient_approved": true }`.
* A rule detects this and **skips the eligibility check**, returning `"Patient is already approved, skipping eligibility check."`.

### Example Webhook Rule

```json
{
  "id": "rule_006",
  "name": "Skip eligibility check for pre-approved patients",
  "trigger": "pre_eligibility_request",
  "conditions": {
    "all": [
      { "field": "patient_approved", "comparison": "equals", "value": true }
    ]
  },
  "actions": [
    {
      "type": "return_custom_response",
      "message": "Patient is already approved, skipping eligibility check."
    }
  ],
  "description": "If patient is pre-approved from external API, do not process eligibility.",
  "webhooks": {
    "pre_eligibility": {
      "method": "POST",
      "url": "https://customer-api.com/pre-eligibility",
      "headers": {
        "Authorization": "Bearer {{aws_secrets:customer_api_key}}",
        "Content-Type": "application/json"
      },
      "timeout": 3000,
      "body": {
        "member_id": "{{card.member_number}}",
        "payer_name": "{{card.payer_name}}"
      }
    }
  }
}
```

Placeholders in webhook configurations (e.g., `{{request.memberId}}`, `{{secrets:customer_api_key}}`) are dynamically resolved when the rule executes. Request-related placeholders map to the incoming payload, while secret placeholders securely fetch stored values.

## Error Handling

If a rule encounters an error during evaluation, Overseer logs the failure and proceeds based on the rule configuration. Webhook calls that fail due to timeouts or HTTP errors can be retried (if configured). Rules can also define fallback behavior for such cases.

### Common Error Scenarios

* **Field not found**: Rule is skipped and logged as a warning. The request continues processing with remaining rules.
* **Webhook timeout**: Rule continues with default behavior. If configured, webhook calls can be retried up to 3 times.
* **Invalid comparison**: Rule fails validation and is skipped. The error is logged and the request continues.
* **Action failure**: Rule logs the error but the request continues processing. Partial modifications may be applied.
* **Rule syntax error**: Rule is disabled until corrected. All other rules continue to function normally.

{% hint style="warning" %}
**Important**: Rule failures are non-blocking. If a rule encounters an error, the eligibility request will continue processing to ensure system reliability.
{% endhint %}

## Logging & Debugging (coming soon)

Overseer provides detailed logs for each rule execution, including:

* **Which rules were evaluated.**
* **Which conditions passed/failed.**
* **How request/response data was modified.**

Clients can enable debug mode for additional visibility when testing new rule configurations.

## Performance & Limits

Overseer is designed for high-throughput processing with the following performance characteristics and limits:

### Execution Limits

* **Rule execution timeout**: 5 seconds per rule
* **Maximum rules per account**: 100 active rules
* **Maximum conditions per rule**: 25 conditions
* **Webhook timeout**: 3 seconds (configurable up to 10 seconds)
* **Maximum webhook retries**: 3 attempts

### Performance Metrics

* **Rule evaluation**: Typically < 50ms per rule
* **Webhook calls**: Add 100-3000ms depending on external API response time
* **Throughput**: Supports 1000+ eligibility requests per minute
* **Latency impact**: Rules add \~100-500ms to eligibility request processing time

### Optimization Tips

* **Keep conditions simple**: Complex nested conditions take longer to evaluate
* **Minimize webhook calls**: Use webhooks only when external data is essential
* **Order rules strategically**: Place frequently-triggered rules first for faster evaluation
* **Use specific triggers**: Avoid rules that run on every request when possible

{% hint style="info" %}
**Performance Monitoring**: Rule execution times and success rates are logged and can be reviewed with the CardScan team for optimization opportunities.
{% endhint %}


# Accessibility & WCAG Compliance 🌐

CardScan.ai's React component is designed to meet WCAG 2.1 Level AA standards, ensuring our insurance card scanning solution is accessible to all users.

## Compliance Status

✅ **WCAG 2.1 Level AA Compliant**

### Comprehensive Testing & Remediation

Our React web component has undergone extensive accessibility testing across all major categories. All identified issues have been resolved to ensure full compliance.

| Test Category           | Status   | Testing Environments                                                                                  |
| ----------------------- | -------- | ----------------------------------------------------------------------------------------------------- |
| **Automated Tools**     | ✅ Passed | Industry-standard accessibility scanners                                                              |
| **Screen Readers**      | ✅ Passed | <p>• Windows 11 / Chrome / NVDA<br>• Android / Chrome / TalkBack<br>• iPhone / Safari / VoiceOver</p> |
| **Color Contrast**      | ✅ Passed | Windows 11 / Chrome                                                                                   |
| **Keyboard Navigation** | ✅ Passed | Windows 11 / Chrome                                                                                   |
| **Browser Zoom**        | ✅ Passed | Windows 11 / Chrome (100% - 200%)                                                                     |
| **Text Spacing**        | ✅ Passed | All supported browsers                                                                                |

### Testing Methodology

Our accessibility testing involved:

1. **Initial Assessment**: Comprehensive testing across all platforms
2. **Issue Identification**: Documentation of all accessibility barriers
3. **Remediation**: Fixed all identified issues
4. **Validation**: Re-tested to confirm compliance

## Key Accessibility Features

### Screen Reader Compatibility

* **Tested and verified** on:
  * NVDA (Windows 11 / Chrome)
  * TalkBack (Android / Chrome)
  * VoiceOver (iPhone / Safari)
* Descriptive ARIA labels on all interactive elements
* Dynamic content changes announced appropriately
* Clear navigation structure and landmarks

### Keyboard Navigation

* All features accessible via keyboard
* Tested on Windows 11 / Chrome
* Logical tab order throughout the interface
* Standard keyboard shortcuts (Enter/Space for activation)
* Visible focus indicators on all interactive elements

### Visual Accessibility

* WCAG AA compliant color contrast ratios verified
* Responsive to browser zoom (100% - 200%)
* Maintains functionality with modified text spacing
* Customizable colors and text sizes via CSS variables

### Mobile Accessibility

* Full support for mobile screen readers
* Touch-friendly interface that works with assistive technologies
* Tested on both Android (TalkBack) and iOS (VoiceOver)

## Customization for Accessibility

The React component provides CSS variables to further enhance accessibility for your specific user needs:

```css
.cardscan-widget {
  /* Adjust for higher contrast if needed */
  --message-text-color: #000000;
  --message-background-color: #ffffff;
  
  /* Increase text size for better readability */
  --message-font-size: 20px;
  --message-font-weight: 600;
  
  /* Ensure interactive elements are easily visible */
  --auto-switch-active-color: #0056b3;
  --progress-bar-color: #28a745;
}
```

## Platform Support

Accessibility features have been tested and verified on:

* **Windows**: Chrome with NVDA screen reader
* **Android**: Chrome with TalkBack
* **iOS**: Safari with VoiceOver
* **macOS**: Safari with VoiceOver (supported)

## Continuous Accessibility Commitment

We are committed to maintaining accessibility standards and regularly test our components to ensure continued compliance as we add new features.

## Accessibility Support

For accessibility-related questions or to report issues, please contact <support@cardscan.ai>.


# MCP Server 🏥

Streamline insurance workflows with AI assistance through the Model Context Protocol (MCP). Connect Claude to your healthcare systems for automated insurance card scanning, eligibility verification, and claims contact extraction.

## Introduction

The Healthcare MCP Server provides HIPAA-compliant AI assistance for common healthcare administrative tasks. By connecting Claude to CardScan.ai's proven insurance processing APIs, healthcare practices can reduce manual verification work and streamline patient intake workflows.

**Key Benefits:**

* **Reduce Manual Work**: Automate insurance verification and card data extraction
* **HIPAA Compliant**: Secure processing with enterprise-grade data protection
* **Real-time Results**: Instant eligibility checks and benefit information
* **Proven Accuracy**: Built on CardScan.ai's battle-tested ML pipeline

**Target Audience:**

* Healthcare practices and billing departments
* Healthcare technology integrators
* Medical practice management systems
* Revenue cycle management companies

## Use Cases & Workflows

### Real-time Eligibility Verification

{% @mermaid/diagram content="sequenceDiagram
participant User
participant Claude
participant Healthcare MCP
participant CardScan API

```
User->>Claude: "Check eligibility for John Doe, member ID 12345"
Claude->>Healthcare MCP: eligibility_lookup()
Healthcare MCP->>CardScan API: POST /eligibility
CardScan API-->>Healthcare MCP: Coverage details
Healthcare MCP-->>Claude: Structured eligibility data
Claude-->>User: "John has active PPO coverage, $500 deductible remaining, 20% coinsurance for office visits..."" %}
```

**Business Impact**: Staff can instantly verify patient coverage without logging into multiple insurance portals, reducing verification time from 5-10 minutes to under 30 seconds.

### Insurance Card Scanning & Contact Extraction

{% @mermaid/diagram content="sequenceDiagram
participant User
participant Claude
participant Healthcare MCP
participant CardScan API

```
User->>Claude: "Extract claims phone number from this insurance card"
Claude->>Healthcare MCP: extract_card_info(image_url)
Healthcare MCP->>CardScan API: POST /cards
Healthcare MCP->>CardScan API: POST /cards/{id}/upload
CardScan API-->>Healthcare MCP: Extracted card data
Healthcare MCP-->>Claude: Claims phone: (800) 555-0123
Claude-->>User: "Claims contact: Aetna Claims (800) 555-0123"
Note over User: Can immediately call or save contact" %}
```

**Business Impact**: Eliminates manual card data entry and reduces phone number lookup time. Perfect for prior authorization calls and claims inquiries.

### Patient Card Update Requests

{% @mermaid/diagram content="sequenceDiagram
participant User
participant Claude
participant Healthcare MCP
participant CardScan API
participant Patient

```
User->>Claude: "Jane's insurance card expired, send her an update link"
Claude->>Healthcare MCP: create_card_update_link(patient_id)
Healthcare MCP->>CardScan API: POST /generate-magic-link
CardScan API-->>Healthcare MCP: Secure upload URL
Healthcare MCP-->>Claude: Generated secure link
Claude-->>User: "Sent Jane a secure link to upload her new card"
Claude->>Patient: SMS/Email with upload link
Note over Patient: Uploads new card securely" %}
```

**Business Impact**: Streamlines patient card collection with secure, time-limited upload links. Reduces staff phone calls and improves patient experience.

## Available Tools & Resources

### Core Tools

#### `eligibility_lookup`

Real-time insurance eligibility verification with comprehensive benefit details.

**Parameters:**

* `member_id` (string, required): Insurance member identification number
* `subscriber_first_name` (string, required): Primary member first name
* `subscriber_last_name` (string, required): Primary member last name
* `date_of_birth` (string, required): Format YYYYMMDD (e.g., "19850315")
* `provider_npi` (string, required): 10-digit National Provider Identifier
* `provider_name` (string, optional): Provider first/last name or organization name
* `service_codes` (array, optional): Specific medical service codes to check

**Returns:**

* Coverage status (active/inactive)
* Deductible information (individual/family, remaining amounts)
* Co-pay amounts by service type
* Co-insurance percentages
* Out-of-pocket maximums
* Prior authorization requirements
* Network status for provider

#### `extract_card_info`

Scan insurance cards and extract structured data including contact information.

**Parameters:**

* `image_url` (string, required): URL of insurance card image (front/back)
* `enable_backside_scan` (boolean, optional): Process both card sides for complete data
* `enable_payer_match` (boolean, optional): Match to canonical payer database

**Returns:**

* Member information (name, ID, DOB, gender)
* Payer details (name, address, phone numbers)
* Plan information (group number, plan name, effective dates)
* Prescription details (BIN, PCN, issuer, plan)
* Claims contact phone numbers
* Customer service contacts
* Confidence scores for each extracted field

#### `create_card_update_link`

Generate secure, time-limited upload links for patients to submit new insurance cards.

**Parameters:**

* `patient_identifier` (string, required): Internal patient ID or email
* `expiration_hours` (integer, optional): Link expiration time (default: 24 hours, max: 168)
* `notification_method` (string, optional): "email" or "sms" for automated delivery
* `custom_message` (string, optional): Personalized message for the patient

**Returns:**

* Secure upload URL (single-use)
* Expiration timestamp
* Magic link identifier for tracking
* Optional notification confirmation

#### `search_member_cards`

Find existing insurance card records by member information or payer details.

**Parameters:**

* `query` (string, required): Search term (member name, payer name, member ID)
* `limit` (integer, optional): Maximum results to return (default: 20, max: 100)
* `date_range` (object, optional): Filter by card scan date
  * `start_date` (string): ISO date format
  * `end_date` (string): ISO date format

**Returns:**

* Array of matching card records
* Card metadata (scan date, processing state)
* Basic member and payer information
* Card IDs for detailed data retrieval

### Available Resources

#### `eligibility/recent`

Browse recent eligibility verification requests across your account.

**Access Pattern:** List recent verifications with pagination support.

#### `eligibility/by_patient/{patient_id}`

Retrieve eligibility verification history for a specific patient.

**Access Pattern:** Get all eligibility checks for a given patient identifier.

#### `card_scans/pending`

Monitor insurance card uploads currently being processed.

**Access Pattern:** Check status of pending card scans and processing queue.

## Getting Started

### Prerequisites

Before integrating the Healthcare MCP Server, ensure you have:

* **HIPAA Compliance**: Your organization must have appropriate HIPAA safeguards in place
* **CardScan.ai Account**: Active account with eligibility verification feature enabled
* **Provider Information**: National Provider Identifier (NPI) for your practice
* **Patient Demographics**: Basic patient information for eligibility requests

{% hint style="info" %}
**NPI Lookup**: Find provider NPIs using the [NPPES NPI Registry](https://npiregistry.cms.hhs.gov/search).
{% endhint %}

### Integration Overview

The Healthcare MCP Server acts as a secure bridge between Claude and CardScan.ai's healthcare APIs:

1. **Authentication**: Server-side API key management with JWT token generation
2. **Data Processing**: HIPAA-compliant handling of protected health information
3. **Real-time Updates**: WebSocket notifications for eligibility and card processing status
4. **Error Handling**: Healthcare-specific error codes and retry logic

### Security & Compliance

* **HIPAA Compliance**: All data processing follows HIPAA requirements
* **Encryption**: End-to-end encryption for all PHI transmission
* **Access Controls**: Role-based access with audit logging
* **Data Retention**: Configurable retention policies for different data types

### Contact for Access

The Healthcare MCP Server is currently in limited preview. To request access:

**Contact the CardScan team** for integration details, pricing, and deployment assistance.

* Include your use case and expected volume
* Specify any custom requirements or integrations needed
* We'll work with you to set up a pilot deployment

{% hint style="success" %}
**Early Access**: We're actively seeking healthcare partners to help refine the MCP server capabilities. Contact us to discuss pilot opportunities.
{% endhint %}

## Example Integration

Here's how the MCP server integrates with your existing workflow:

```python
# Example: Staff uses Claude for eligibility check
claude_query = """
Check eligibility for Sarah Johnson, DOB 03/15/1985, 
member ID SJ123456789, using our main NPI 1234567890.
"""

# MCP server handles the API calls automatically
# Returns formatted eligibility summary in natural language
```

The MCP server abstracts away API complexity while providing rich, structured healthcare data that Claude can interpret and present in user-friendly formats.


# AI Development 🤖

## Overview

We're big fans of using AI tools for development - when done safely. We've created two context files to help AI coding assistants understand our API and generate accurate code.

## Context Files

[**llms.txt**](https://docs.cardscan.ai/llms.txt) - Table of contents for quick navigation\
[**llms-full.txt**](https://docs.cardscan.ai/llms-full.txt) - Complete API documentation and examples

Use `llms.txt` when you need to find something quickly. Use `llms-full.txt` when implementing features or debugging.

## Additional Resources

* [**OpenAPI Spec**](https://github.com/CardScan-ai/api-clients/blob/main/openapi.yaml) - For generating clients in any language
* [**API Clients**](https://github.com/CardScan-ai/api-clients) - Official client libraries for TypeScript, Python, Swift, Kotlin, and Dart

{% hint style="success" %}
**AI in Production:** Check out [Claude Code Watchdog](https://github.com/CardScan-ai/claude-code-watchdog) - a 100% AI-generated tool we use daily. It filters out flaky test noise so we can focus on real bugs.
{% endhint %}


# Testing Resources 🧪

## Overview

CardScan.ai maintains a [public testing repository](https://github.com/CardScan-ai/testing-resources) with sample insurance cards and test videos for validating your integration. These resources help ensure consistent testing across development, staging, and CI/CD environments.

{% hint style="info" %}
All test cards are synthetic samples for testing purposes only. They do not contain real patient information.
{% endhint %}

## Quick Start

1. **Clone the repository:**

   ```bash
   git clone https://github.com/CardScan-ai/testing-resources.git
   ```
2. **Use sample cards** from `insurance-card-images/` for testing
3. **Use test videos** from `insurance-test-videos/` for automated testing

## Browser Testing with Fake Webcam

### Chrome Launch Flags

Test with consistent video input instead of a live webcam:

```bash
# macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
  --use-fake-device-for-media-stream \
  --use-file-for-fake-video-capture=./path/to/test-video.y4m

# Windows
chrome.exe \
  --use-fake-device-for-media-stream \
  --use-file-for-fake-video-capture=./path/to/test-video.y4m

# Linux
google-chrome \
  --use-fake-device-for-media-stream \
  --use-file-for-fake-video-capture=./path/to/test-video.y4m
```

### Cypress Integration

Configure Cypress to use test videos as webcam input:

**1. Update `cypress/plugins/index.js`:**

```javascript
module.exports = (on, config) => {
  on('before:browser:launch', (browser = {}, launchOptions) => {
    if (browser.family === 'chromium' && browser.name !== 'electron') {
      if (process.env.CYPRESS_WEBCAM_VIDEO_PATH) {
        console.log("Setting Video to be: ", process.env.CYPRESS_WEBCAM_VIDEO_PATH)
        launchOptions.args.push(
          `--use-file-for-fake-video-capture=${process.env.CYPRESS_WEBCAM_VIDEO_PATH}`
        )
      }
    }
    return launchOptions
  })
}
```

**2. Run tests with video input:**

```bash
CYPRESS_WEBCAM_VIDEO_PATH=./testing-resources/insurance-test-videos/1080p.y4m cypress run
```

**3. Example test:**

```javascript
describe('Card Scanning', () => {
  it('should scan test card successfully', () => {
    cy.visit('/scan')
    
    // Your CardScan implementation will receive video frames
    // from the test video instead of live webcam
    cy.get('[data-testid="scan-button"]').click()
    
    // Wait for scan completion
    cy.get('[data-testid="scan-complete"]', { timeout: 10000 })
      .should('be.visible')
  })
})
```

## Testing Best Practices

### 1. Test Multiple Scenarios

* Different card types and payers
* Various video resolutions
* Front and back card scanning
* Error conditions and edge cases

### 2. Continuous Integration

Example GitHub Actions workflow:

```yaml
name: E2E Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup test resources
        run: |
          git clone https://github.com/CardScan-ai/testing-resources.git
          cd testing-resources/insurance-test-videos
          unzip videos.zip
      
      - name: Run tests
        env:
          CYPRESS_WEBCAM_VIDEO_PATH: ./testing-resources/insurance-test-videos/1080p.y4m
        run: |
          npm install
          npm run test:e2e
```

## Available Resources

The testing repository includes:

* **Sample card images** - High-quality insurance card images (front/back)
* **Test videos** - Pre-recorded scanning videos in multiple resolutions
* **Additional resources** - Check the repository for the latest test materials

{% hint style="warning" %}
The repository contents may change over time. Always check the [latest README](https://github.com/CardScan-ai/testing-resources/blob/main/README.md) for current resources.
{% endhint %}

## Related Documentation

* [React SDK Testing](/ui-components/react#testing)
* [React Native SDK Testing](/ui-components/react-native#testing)
* [API Testing](/api#testing-endpoints)
* [Webhooks Testing](https://github.com/CardScan-ai/cardscan-documentation/blob/master/developer-tools/webhooks.md#testing-webhooks)

## Support

Need help with testing?

* **Email**: <support@cardscan.ai>
* **Slack**: Available for Enterprise customers


