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)

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

Gradle (Groovy)

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

Maven

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

Basic Usage

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

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

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

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

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

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

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

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

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

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

// 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

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

Last updated

Was this helpful?