Skip to main content

Nafsi Android SDK

Nafsi Android SDK for identity verification powered by Nafsi AI. Easily integrate secure identity verification into your Android app.

Before You Start

Ensure your project meets these exact requirements:

RequirementMinimum VersionRecommended
Android Gradle Plugin8.0.08.2.2
Gradle8.08.2
Kotlin1.9.01.9.20
compileSdk3434
minSdk2424
targetSdk3434
Java1717
Compose Compiler1.5.41.5.4

Checklist before integration:

  • Your project uses Jetpack Compose (not XML views)
  • You have compileSdk 34 or higher
  • You have Java 17 configured
  • You have a valid JWT token from Nafsi

Installation

Step 1: Configure Gradle Settings

Open your settings.gradle.kts file (in your project root):

// settings.gradle.kts
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://maven.nafsi.ai/releases") }
}
}

rootProject.name = "YourAppName"
include(":app")
Using Groovy? Click here for settings.gradle
// settings.gradle
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://maven.nafsi.ai/releases") }
}
}

rootProject.name = "YourAppName"
include ':app'

Step 2: Configure Root build.gradle.kts

Open your root build.gradle.kts file (in project root, NOT in app folder):

// build.gradle.kts (Project root)
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}
Using Groovy? Click here for build.gradle
// build.gradle (Project root)
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

Step 3: Configure App build.gradle.kts

Open your app/build.gradle.kts file and add the SDK dependency. Here is a complete working example:

// app/build.gradle.kts
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}

android {
namespace = "com.yourcompany.yourapp" // Change to your package name
compileSdk = 34

defaultConfig {
applicationId = "com.yourcompany.yourapp" // Change to your app ID
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0.0"
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}

// IMPORTANT: Must be Java 17
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

// IMPORTANT: Must match Java version
kotlinOptions {
jvmTarget = "17"
}

// IMPORTANT: Compose must be enabled
buildFeatures {
compose = true
}

// IMPORTANT: Must match Kotlin version 1.9.20
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
}

packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}

dependencies {
// ========================================
// NAFSI SDK - Add this line
// ========================================
implementation("ai.nafsi:nafsi-sdk:1.0.3")

// ========================================
// Required: Jetpack Compose (use BOM for version alignment)
// ========================================
implementation(platform("androidx.compose:compose-bom:2023.10.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")

// ========================================
// Required: AndroidX Core
// ========================================
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.activity:activity-compose:1.8.1")

// ========================================
// Required: Retrofit for API calls (to fetch JWT token)
// ========================================
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")

// ========================================
// Debug tools (optional but recommended)
// ========================================
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
Using Groovy? Click here for app/build.gradle
// app/build.gradle
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}

android {
namespace 'com.yourcompany.yourapp'
compileSdk 34

defaultConfig {
applicationId "com.yourcompany.yourapp"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0.0"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = '17'
}

buildFeatures {
compose true
}

composeOptions {
kotlinCompilerExtensionVersion = '1.5.4'
}

packaging {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}

dependencies {
// Nafsi SDK
implementation 'ai.nafsi:nafsi-sdk:1.0.3'

// Jetpack Compose
implementation platform('androidx.compose:compose-bom:2023.10.01')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'

// AndroidX
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation 'androidx.activity:activity-compose:1.8.1'

// Retrofit for API calls (to fetch JWT token)
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'

debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
}

Step 4: Configure gradle.properties

Open your gradle.properties file (in project root) and ensure these settings:

# gradle.properties
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true

Step 5: Add Permissions

Open your app/src/main/AndroidManifest.xml and add the required permissions:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<!-- REQUIRED: Camera for document and selfie capture -->
<uses-permission android:name="android.permission.CAMERA"/>

<!-- REQUIRED: Internet for verification API -->
<uses-permission android:name="android.permission.INTERNET"/>

<!-- REQUIRED: Camera hardware feature -->
<uses-feature
android:name="android.hardware.camera"
android:required="true"/>

<application
android:usesCleartextTraffic="true"> <!-- Required for local development (HTTP) -->
<!-- Your existing application config -->
</application>

</manifest>

Note: android:usesCleartextTraffic="true" is only needed for local development with HTTP. Remove this or set to false in production when using HTTPS.


Step 6: Sync and Build

  1. Click "Sync Now" in the yellow banner at the top of Android Studio
  2. Wait for Gradle sync to complete
  3. Build your project: Build → Make Project (or Ctrl+F9 / Cmd+F9)

If the build succeeds, proceed to Step 7. If you get errors, see Troubleshooting.


Step 7 & 8: Backend Authentication Setup

Before your mobile app can use the Nafsi SDK, you need to set up backend authentication. This is a two-part process:

┌─────────────────────────────────────────────────────────────────────────────┐
│ AUTHENTICATION FLOW OVERVIEW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ STEP 7 (One-Time Setup) STEP 8 (Runtime) │
│ ───────────────────────── ───────────────── │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Your Server │ ──POST──► │ Mobile App │───►│ Your Backend │ │
│ └──────────────┘ │ └──────────────┘ └──────────────┘ │
│ │ │ ▲ │ │
│ │ ▼ │ │ │
│ │ ┌────────────┐ │ ▼ │
│ │ │ Nafsi API │ │ ┌────────────┐ │
│ │ └────────────┘ │ │ Generate │ │
│ │ │ │ │ JWT Token │ │
│ │ │ │ │ (includes │ │
│ │ ▼ │ │ refresh_ │ │
│ │ ┌────────────────┐ │ │ token) │ │
│ │ │ Returns JWT │ │ └────────────┘ │
│ │ │ with refresh_ │ │ │ │
│ │ │ token inside │ └───────────────────┘ │
│ │ └────────────────┘ Return JWT │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────────┐ │
│ │ Save refresh_token to .env │ │
│ │ (Use in Step 8) │ │
│ └────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

Step 7: Generate Refresh Token (One-Time Setup)

First, obtain a refresh token from the Nafsi API. You only need to do this once during initial setup.

API Request

Make a POST request to generate your access and refresh tokens:

curl -X POST https://apisv2.windeal.co.ke/postdata \
-H "Content-Type: application/json" \
-d '{
"method": "generateAccessToken",
"menu": "auth_config",
"client_secret": "YOUR_CLIENT_SECRET",
"client_id": "YOUR_CLIENT_ID",
"rpcQueue": "nafsi"
}'

Request Body

FieldTypeRequiredDescription
methodStringYesMust be "generateAccessToken"
menuStringYesMust be "auth_config"
client_secretStringYesYour client secret from Nafsi Dashboard
client_idStringYesYour client ID from Nafsi Dashboard
rpcQueueStringYesMust be "nafsi"

Response

The API returns a JWT token in the result field:

{
"method": "generateAccessToken",
"menu": "auth_config",
"client_id": "your-client-id",
"result": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Extract the Refresh Token

Decode the JWT token from the result field to extract the refresh token. The decoded payload contains:

{
"data": {
"expires_at": "2026-03-17 07:57:03.000000",
"expires_in": 3600,
"token_type": "Bearer",
"access_token": "83049a96882286...",
"refresh_token": "8d85fa5e89818f4dcd62593f01924068..."
},
"status": "success",
"iat": 1773730623,
"exp": 1773734223
}

Save the refresh_token value - you'll use this in Step 8 for backend token generation.

Decode JWT in Different Languages

Node.js
const jwt = require('jsonwebtoken');

const result = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // From API response
const decoded = jwt.decode(result);
const refreshToken = decoded.data.refresh_token;

console.log("Refresh Token:", refreshToken);
// Save this to your environment variables
Python
import jwt

result = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." # From API response
decoded = jwt.decode(result, options={"verify_signature": False})
refresh_token = decoded["data"]["refresh_token"]

print(f"Refresh Token: {refresh_token}")
# Save this to your environment variables
Java
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.gson.Gson;
import com.google.gson.JsonObject;

import java.util.Base64;

public class JwtDecoder {
public static void main(String[] args) {
String result = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // From API response

// Decode JWT without verification
DecodedJWT jwt = JWT.decode(result);

// Get payload and decode from Base64
String payload = new String(Base64.getUrlDecoder().decode(jwt.getPayload()));

// Parse JSON to extract refresh_token
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(payload, JsonObject.class);
String refreshToken = jsonObject
.getAsJsonObject("data")
.get("refresh_token")
.getAsString();

System.out.println("Refresh Token: " + refreshToken);
// Save this to your environment variables
}
}

Maven dependency:


<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
Spring Boot
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;

import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

@Service
public class NafsiTokenService {

private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();

public String generateRefreshToken(String clientId, String clientSecret) {
String url = "https://apisv2.windeal.co.ke/postdata";

// Build request body
Map<String, String> requestBody = new HashMap<>();
requestBody.put("method", "generateAccessToken");
requestBody.put("menu", "auth_config");
requestBody.put("client_secret", clientSecret);
requestBody.put("client_id", clientId);
requestBody.put("rpcQueue", "nafsi");

// Set headers
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<Map<String, String>> entity = new HttpEntity<>(requestBody, headers);

// Make POST request
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);

// Extract JWT from result field
String jwtResult = (String) response.getBody().get("result");

// Decode JWT to get refresh token
DecodedJWT jwt = JWT.decode(jwtResult);
String payload = new String(Base64.getUrlDecoder().decode(jwt.getPayload()));

JsonNode jsonNode = objectMapper.readTree(payload);
String refreshToken = jsonNode.get("data").get("refresh_token").asText();

System.out.println("Refresh Token: " + refreshToken);
// Save this to your application.properties or environment variables

return refreshToken;
}
}

Maven dependencies:


<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
PHP
<?php

// Using firebase/php-jwt library
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$result = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // From API response

// Decode JWT without verification (just to read payload)
$parts = explode('.', $result);
$payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true);

$refreshToken = $payload['data']['refresh_token'];

echo "Refresh Token: " . $refreshToken . "\n";
// Save this to your .env file

// Alternative: Using firebase/php-jwt with full decode
// composer require firebase/php-jwt
// $decoded = JWT::decode($result, new Key($secretKey, 'HS256'));
// $refreshToken = $decoded->data->refresh_token;

Composer dependency:

composer require firebase/php-jwt

Full PHP example with API call:

<?php

function generateRefreshToken($clientId, $clientSecret) {
$url = "https://apisv2.windeal.co.ke/postdata";

$data = [
"method" => "generateAccessToken",
"menu" => "auth_config",
"client_secret" => $clientSecret,
"client_id" => $clientId,
"rpcQueue" => "nafsi"
];

$options = [
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => json_encode($data)
]
];

$context = stream_context_create($options);
$response = file_get_contents($url, false, $context);
$responseData = json_decode($response, true);

// Decode the JWT result
$jwtResult = $responseData['result'];
$parts = explode('.', $jwtResult);
$payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true);

$refreshToken = $payload['data']['refresh_token'];

echo "Refresh Token: " . $refreshToken . "\n";
// Save to your .env file: NAFSI_REFRESH_TOKEN=...

return $refreshToken;
}

// Usage
$refreshToken = generateRefreshToken('your-client-id', 'your-client-secret');
Online Tool

You can also use jwt.io to decode the token:

  1. Go to https://jwt.io
  2. Paste the result JWT token in the "Encoded" field
  3. The decoded payload will show in the right panel
  4. Copy the refresh_token from data.refresh_token

Important: Store the refresh token securely as an environment variable on your server. Never expose it in client-side code. You'll use this refresh token in Step 8 below.


Step 8: Create Backend Token Endpoint

Now that you have the refresh_token from Step 7, create a backend endpoint that generates JWT tokens for the mobile SDK.

Why a backend endpoint? The Nafsi SDK requires a JWT token containing sensitive credentials. These must never be stored in mobile apps - always generate tokens server-side.

┌──────────────┐    1. Request Token     ┌────────────────┐
│ Mobile App │ ──────────────────────► │ Your Backend │
│ (Nafsi SDK) │ │ Server │
└──────────────┘ └────────────────┘
▲ │
│ Uses refresh_token
│ from Step 7
│ │
│ ▼
│ ┌────────────────┐
│ 2. Return JWT Token │ Generate JWT │
└──────────────────────────────│ with claims │
└────────────────┘

Node.js / Express Example

const express = require("express");
const jwt = require("jsonwebtoken");

const app = express();

// Protect this endpoint with your authentication middleware
app.get("/generate-nafsi-token", authMiddleware, (req, res) => {
const userId = req.user.id; // From your auth middleware

const payload = {
workflowId: process.env.NAFSI_WORKFLOW_ID, // From Nafsi Dashboard
clientId: process.env.NAFSI_CLIENT_ID, // From Nafsi Dashboard
config: "ke_national_id", // Document type
apiUrl: process.env.NAFSI_API_URL, // Nafsi API URL
organisationId: process.env.NAFSI_ORG_ID, // Your organization ID
refresh_token: process.env.NAFSI_REFRESH_TOKEN, // From Step 7 above
sub: userId // Optional: Include user context for audit trails
};

const token = jwt.sign(payload, process.env.NAFSI_JWT_SECRET, {
expiresIn: "24h"
});

res.json({token});
});

app.listen(3000);
Python / Flask Example
from flask import Flask, jsonify, request
from flask_jwt_extended import jwt_required, get_jwt_identity
import jwt
import os
from datetime import datetime, timedelta

app = Flask(__name__)

@app.route("/generate-nafsi-token", methods=["GET"])
@jwt_required() # Your auth decorator
def generate_nafsi_token():
user_id = get_jwt_identity()

payload = {
"workflowId": os.environ["NAFSI_WORKFLOW_ID"],
"clientId": os.environ["NAFSI_CLIENT_ID"],
"config": "ke_national_id",
"apiUrl": os.environ["NAFSI_API_URL"],
"organisationId": os.environ["NAFSI_ORG_ID"],
"refresh_token": os.environ["NAFSI_REFRESH_TOKEN"],
"sub": user_id,
"exp": datetime.utcnow() + timedelta(hours=24)
}

token = jwt.encode(payload, os.environ["NAFSI_JWT_SECRET"], algorithm="HS256")
return jsonify({"token": token})
PHP / Laravel Example
<?php

use Firebase\JWT\JWT;
use Illuminate\Http\Request;

Route::middleware('auth:sanctum')->get('/generate-nafsi-token', function (Request $request) {
$payload = [
'workflowId' => env('NAFSI_WORKFLOW_ID'),
'clientId' => env('NAFSI_CLIENT_ID'),
'config' => 'ke_national_id',
'apiUrl' => env('NAFSI_API_URL'),
'organisationId' => env('NAFSI_ORG_ID'),
'refresh_token' => env('NAFSI_REFRESH_TOKEN'),
'sub' => $request->user()->id,
'exp' => time() + (24 * 60 * 60) // 24 hours
];

$token = JWT::encode($payload, env('NAFSI_JWT_SECRET'), 'HS256');

return response()->json(['token' => $token]);
});
Java / Spring Boot Example
package com.yourcompany.service;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@RestController
public class NafsiTokenController {

@Value("${nafsi.workflow-id}")
private String workflowId;

@Value("${nafsi.client-id}")
private String clientId;

@Value("${nafsi.api-url}")
private String apiUrl;

@Value("${nafsi.org-id}")
private String organisationId;

@Value("${nafsi.refresh-token}")
private String refreshToken;

@Value("${nafsi.jwt-secret}")
private String jwtSecret;

@GetMapping("/generate-nafsi-token")
public ResponseEntity<Map<String, String>> generateNafsiToken(
@AuthenticationPrincipal UserDetails userDetails) {

Algorithm algorithm = Algorithm.HMAC256(jwtSecret);

String token = JWT.create()
.withClaim("workflowId", workflowId)
.withClaim("clientId", clientId)
.withClaim("config", "ke_national_id")
.withClaim("apiUrl", apiUrl)
.withClaim("organisationId", organisationId)
.withClaim("refresh_token", refreshToken)
.withSubject(userDetails.getUsername()) // Optional: user context
.withExpiresAt(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) // 24 hours
.sign(algorithm);

Map<String, String> response = new HashMap<>();
response.put("token", token);

return ResponseEntity.ok(response);
}
}

application.properties:

# Nafsi Configuration (use environment variables in production)
nafsi.workflow-id=${NAFSI_WORKFLOW_ID}
nafsi.client-id=${NAFSI_CLIENT_ID}
nafsi.api-url=${NAFSI_API_URL}
nafsi.org-id=${NAFSI_ORG_ID}
nafsi.refresh-token=${NAFSI_REFRESH_TOKEN}
nafsi.jwt-secret=${NAFSI_JWT_SECRET}

Maven dependencies (pom.xml):


<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
</dependencies>
Java / Servlet Example (Non-Spring)
package com.yourcompany.servlet;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.google.gson.Gson;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@WebServlet("/generate-nafsi-token")
public class NafsiTokenServlet extends HttpServlet {

private static final String WORKFLOW_ID = System.getenv("NAFSI_WORKFLOW_ID");
private static final String CLIENT_ID = System.getenv("NAFSI_CLIENT_ID");
private static final String API_URL = System.getenv("NAFSI_API_URL");
private static final String ORG_ID = System.getenv("NAFSI_ORG_ID");
private static final String REFRESH_TOKEN = System.getenv("NAFSI_REFRESH_TOKEN");
private static final String JWT_SECRET = System.getenv("NAFSI_JWT_SECRET");

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {

// Get authenticated user (implement your auth logic)
String userId = (String) request.getAttribute("userId");

Algorithm algorithm = Algorithm.HMAC256(JWT_SECRET);

String token = JWT.create()
.withClaim("workflowId", WORKFLOW_ID)
.withClaim("clientId", CLIENT_ID)
.withClaim("config", "ke_national_id")
.withClaim("apiUrl", API_URL)
.withClaim("organisationId", ORG_ID)
.withClaim("refresh_token", REFRESH_TOKEN)
.withSubject(userId)
.withExpiresAt(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000))
.sign(algorithm);

Map<String, String> result = new HashMap<>();
result.put("token", token);

response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(new Gson().toJson(result));
}
}

Maven dependencies:


<dependencies>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
PHP (Plain / Non-Laravel)
<?php

require_once 'vendor/autoload.php';

use Firebase\JWT\JWT;

// Load environment variables (use dotenv or similar in production)
$workflowId = getenv('NAFSI_WORKFLOW_ID');
$clientId = getenv('NAFSI_CLIENT_ID');
$apiUrl = getenv('NAFSI_API_URL');
$orgId = getenv('NAFSI_ORG_ID');
$refreshToken = getenv('NAFSI_REFRESH_TOKEN');
$jwtSecret = getenv('NAFSI_JWT_SECRET');

// Your authentication logic here
session_start();
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}

$userId = $_SESSION['user_id'];

$payload = [
'workflowId' => $workflowId,
'clientId' => $clientId,
'config' => 'ke_national_id',
'apiUrl' => $apiUrl,
'organisationId' => $orgId,
'refresh_token' => $refreshToken,
'sub' => $userId,
'exp' => time() + (24 * 60 * 60) // 24 hours
];

$token = JWT::encode($payload, $jwtSecret, 'HS256');

header('Content-Type: application/json');
echo json_encode(['token' => $token]);

Composer dependency:

composer require firebase/php-jwt vlucas/phpdotenv

Environment Variables

Store your Nafsi credentials as environment variables on your server. The NAFSI_REFRESH_TOKEN is the value you obtained in Step 7.

# .env (Server-side only - NEVER commit to git)
NAFSI_WORKFLOW_ID=your-workflow-id # From Nafsi Dashboard
NAFSI_CLIENT_ID=your-client-id # From Nafsi Dashboard
NAFSI_API_URL=https://api.nafsi.ai # Nafsi API URL
NAFSI_ORG_ID=your-org-id # From Nafsi Dashboard
NAFSI_REFRESH_TOKEN=8d85fa5e89818f4d... # From Step 7 above
NAFSI_JWT_SECRET=your-jwt-secret # Your own secret for signing JWTs

JWT Payload Reference

FieldTypeRequiredDescription
workflowIdStringYesYour workflow ID from Nafsi Dashboard
clientIdStringYesYour client ID from Nafsi Dashboard
configStringYesDocument type (e.g., ke_national_id, ke_passport)
apiUrlStringYesNafsi API URL
organisationIdStringYesYour organization ID
refresh_tokenStringYesFrom Step 7 - the token you extracted
subStringNoUser identifier for audit trails
expNumberNoExpiration time (Unix timestamp)

Security Best Practices

  • Never hardcode secrets in mobile apps - Always fetch tokens from your backend
  • Authenticate users first - Only generate tokens for authenticated users
  • Use short expiration times - 24 hours or less is recommended
  • Use HTTPS - Always use HTTPS for token requests
  • Rate limiting - Implement rate limiting on your token endpoint

Quick Start

Minimal Integration

First, create an API service to fetch the token from your backend:

// ApiService.kt
import retrofit2.http.GET

interface ApiService {
@GET("generate-nafsi-token")
suspend fun getNafsiToken(): NafsiTokenResponse
}

data class NafsiTokenResponse(val token: String)

// Retrofit instance
object RetrofitClient {
// For Android Emulator: use 10.0.2.2 instead of localhost
// For physical device: use your computer's local IP (e.g., 192.168.x.x)
// For production: use your actual server URL
private const val BASE_URL = "http://10.0.2.2:3000/"

val apiService: ApiService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}

Then add this to your Composable screen:

import ai.nafsi.sdk.ui.NafsiVerificationView
import ai.nafsi.sdk.models.NafsiConfig

@Composable
fun VerificationScreen(jwtToken: String) {
NafsiVerificationView(
config = NafsiConfig(token = jwtToken),
onSuccess = { result ->
println("Success: ${result.verificationId}")
},
onError = { error ->
println("Error: ${error.message}")
},
onCancel = {
println("User cancelled")
}
)
}

Complete Working Example

Copy this entire file as your MainActivity.kt:

package com.yourcompany.yourapp  // Change to your package name

import ai.nafsi.sdk.models.NafsiConfig
import ai.nafsi.sdk.models.NafsiCustomization
import ai.nafsi.sdk.ui.NafsiVerificationView
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject

private const val BASE_URL = "http://10.0.2.2:3000/generate-nafsi-token"

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
VerificationApp()
}
}
}
}
}

@Composable
fun VerificationApp() {
var showSDK by remember { mutableStateOf(true) }
var resultMessage by remember { mutableStateOf<String?>(null) }
var jwtToken by remember { mutableStateOf<String?>(null) }
var isLoading by remember { mutableStateOf(true) }
var tokenError by remember { mutableStateOf<String?>(null) }

LaunchedEffect(Unit) {
try {
jwtToken = fetchToken()
isLoading = false
} catch (e: Exception) {
tokenError = "Failed to fetch token: ${e.message}"
isLoading = false
}
}

if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
Text("Loading token...")
}
}
return
}

if (tokenError != null) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = tokenError!!, color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
isLoading = true
tokenError = null
}) {
Text("Retry")
}
}
return
}

if (showSDK && jwtToken != null) {
val config = NafsiConfig(
token = jwtToken!!,
customization = NafsiCustomization(
primaryColor = "#10b981",
secondaryColor = "#065f46",
title = "Identity Verification",
tagline = "Quick and secure verification"
)
)

NafsiVerificationView(
config = config,
onSuccess = { result ->
resultMessage = "Success! ID: ${result.verificationId}"
showSDK = false
},
onError = { error ->
resultMessage = "Error: ${error.message}"
showSDK = false
},
onCancel = {
resultMessage = "Cancelled"
showSDK = false
}
)
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(
onClick = { showSDK = true },
modifier = Modifier.fillMaxWidth()
) {
Text("Start Verification")
}

resultMessage?.let { message ->
Spacer(modifier = Modifier.height(16.dp))
Text(text = message)
}
}
}
}

private suspend fun fetchToken(): String {
return withContext(Dispatchers.IO) {
val client = OkHttpClient()
val request = Request.Builder()
.url(BASE_URL)
.build()

val response = client.newCall(request).execute()
if (!response.isSuccessful) {
throw Exception("HTTP ${response.code}")
}

val responseBody = response.body?.string() ?: throw Exception("Empty response")
val json = JSONObject(responseBody)
json.getString("token")
}
}

For production apps, use a ViewModel to manage state:

// VerificationViewModel.kt
class VerificationViewModel : ViewModel() {

private val _nafsiToken = MutableStateFlow<String?>(null)
val nafsiToken: StateFlow<String?> = _nafsiToken

private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading

private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error

fun fetchNafsiToken() {
viewModelScope.launch {
_isLoading.value = true
_error.value = null
try {
// Fetch from: http://localhost:3000/generate-nafsi-token
val response = RetrofitClient.apiService.getNafsiToken()
_nafsiToken.value = response.token
} catch (e: Exception) {
_error.value = e.message
} finally {
_isLoading.value = false
}
}
}

fun clearToken() {
_nafsiToken.value = null
}
}

// VerificationScreen.kt
@Composable
fun VerificationScreen(viewModel: VerificationViewModel = viewModel()) {
val token by viewModel.nafsiToken.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
val error by viewModel.error.collectAsState()

when {
isLoading -> CircularProgressIndicator()
token != null -> {
NafsiVerificationView(
config = NafsiConfig(token = token!!),
onSuccess = { result ->
// Handle success
viewModel.clearToken()
},
onError = { error ->
// Handle error
viewModel.clearToken()
},
onCancel = {
viewModel.clearToken()
}
)
}
else -> {
Button(onClick = { viewModel.fetchNafsiToken() }) {
Text("Start Verification")
}
error?.let { Text("Error: $it", color = Color.Red) }
}
}
}

Troubleshooting Build Issues

Error: "Could not resolve ai.nafsi:nafsi-sdk:1.0.3"

Cause: Maven Central repository is not configured or there's a network issue.

Fix: Ensure mavenCentral() is in your settings.gradle.kts:

dependencyResolutionManagement {
repositories {
google()
mavenCentral() // Nafsi SDK is hosted here
}
}

Then click "Sync Now".

You can verify the SDK exists at: https://repo1.maven.org/maven2/ai/nafsi/nafsi-sdk/1.0.0/


Error: "Unresolved reference: NafsiVerificationView" or "Cannot find symbol"

Cause: The SDK is not imported correctly or build failed.

Fix:

  1. Ensure implementation("ai.nafsi:nafsi-sdk:1.0.3") is in your dependencies
  2. Click File → Sync Project with Gradle Files
  3. Click Build → Rebuild Project
  4. Add the correct imports at the top of your Kotlin file:
import ai.nafsi.sdk.ui.NafsiVerificationView
import ai.nafsi.sdk.models.NafsiConfig
import ai.nafsi.sdk.models.NafsiCustomization

Error: "Execution failed for task ':app:compileDebugKotlin'" with Compose errors

Cause: Kotlin and Compose compiler version mismatch.

Fix: Ensure these versions match in your app/build.gradle.kts:

// In plugins section of root build.gradle.kts
id("org.jetbrains.kotlin.android") version "1.9.20"

// In android block of app/build.gradle.kts
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4" // Must match Kotlin 1.9.20
}

Version compatibility table:

Kotlin VersionCompose Compiler Version
1.9.201.5.4
1.9.101.5.3
1.9.01.5.1
1.8.221.4.8

Still Having Issues?

  1. Clean and rebuild:

    ./gradlew clean
    ./gradlew assembleDebug
  2. Invalidate caches: File → Invalidate Caches → Invalidate and Restart

  3. Delete .gradle folders:

    • Delete ~/.gradle/caches/ (user home)
    • Delete <project>/.gradle/ (project folder)
    • Sync project again
  4. Contact support: support@nafsi.ai with your error log


JWT Token

See Step 7: Generate Refresh Token and Step 8: Backend Token Generation Setup for complete documentation on setting up JWT token generation.

Key Points:

  • Generate tokens on your backend server, never in the mobile app
  • Tokens expire after 24 hours - always fetch a fresh token before verification
  • Authenticate users before generating tokens
  • Store credentials as environment variables on your server

Customization

Theme Colors

NafsiCustomization(
primaryColor = "#10b981", // Buttons, accents
secondaryColor = "#065f46", // Secondary elements
buttonTextColor = "#FFFFFF", // Button text
successColor = "#17D27C", // Success screen
errorColor = "#FF4842" // Error screen
)

Branding

NafsiCustomization(
logoUrl = "https://your-company.com/logo.png",
title = "Your Company Name",
tagline = "Your tagline here",
showLogo = true
)

Handling Results

The SDK provides three callbacks to handle verification outcomes. These callbacks are triggered when the user closes the success/error screen, allowing users to see their verification result before your app takes over.

Callback Overview

NafsiVerificationView(
config = config,
onSuccess = { result: VerificationResult ->
// Called when verification succeeds and user closes the success screen
// Full verification data is available in the result object
},
onError = { error: NafsiError ->
// Called when verification fails and user closes the error screen
// Typed error with error code and message
},
onCancel = {
// Called when user cancels the verification flow at any step
}
)

Complete Success Handling Example

onSuccess = { result ->
// Basic verification info
val verificationId = result.verificationId
val status = result.status

// Check success using helper method
if (result.isSuccess()) {
Log.d("Verification", "Success! ID: $verificationId")
}

// Extract ID document data
result.extractedData?.let { data ->
val fullName = data.fullName
val idNumber = data.idNumber ?: data.nationalIdNumber
val dateOfBirth = data.dateOfBirth
val gender = data.gender

Log.d("Verification", "Name: $fullName, ID: $idNumber")
}

// Check face matching results
result.photoComparison?.let { photo ->
val facesMatch = photo.match ?: false
val confidence = photo.confidence ?: 0.0

Log.d("Verification", "Face match: $facesMatch (${confidence * 100}%)")
}

// Check liveness detection
result.liveness?.let { liveness ->
val isLive = liveness.isLive
val confidence = liveness.confidence

Log.d("Verification", "Liveness: $isLive (${confidence * 100}%)")
}

// Check age verification
result.ageVerification?.let { age ->
val userAge = age.age
val isAdult = age.isAdult

Log.d("Verification", "Age: $userAge, Adult: $isAdult")
}

// Get selfie URL if needed
result.selfieUrl?.let { url ->
Log.d("Verification", "Selfie URL: $url")
}

// Proceed with your app logic
navigateToNextScreen(result)
}

Error Handling

The SDK uses typed errors for precise error handling:

onError = { error ->
// Get error code for programmatic handling
val errorCode = error.getErrorCode()
val message = error.message

Log.e("Verification", "Error [$errorCode]: $message")

when (error) {
// Token errors
is NafsiError.InvalidJWT -> {
// Token is malformed or invalid
showError("Invalid verification token")
}
is NafsiError.JWTExpired -> {
// Token has expired - fetch a new one
refreshTokenAndRetry()
}
is NafsiError.JWTDecodeFailed -> {
// Failed to decode the token
showError("Token decode failed")
}

// Network errors
is NafsiError.NetworkError -> {
// No internet connection
showError("Please check your internet connection")
}
is NafsiError.TimeoutError -> {
// Request timed out
showError("Request timed out. Please try again.")
}
is NafsiError.ApiError -> {
// API returned an error
val statusCode = error.statusCode
showError("Server error: $statusCode")
}

// Camera/Permission errors
is NafsiError.CameraPermissionDenied -> {
// User denied camera permission
requestCameraPermission()
}
is NafsiError.CameraNotAvailable -> {
// Device camera not available
showError("Camera not available on this device")
}
is NafsiError.CameraError -> {
// Camera error during capture
showError("Camera error: ${error.message}")
}

// Verification errors
is NafsiError.VerificationFailed -> {
// Verification was rejected
showError("Verification failed: ${error.message}")
}
is NafsiError.VerificationRejected -> {
// Document or face was rejected
showError("Verification was rejected")
}

// Image errors
is NafsiError.ImageProcessingError -> {
showError("Failed to process image")
}

// Workflow errors
is NafsiError.WorkflowFetchFailed -> {
showError("Failed to load verification workflow")
}

// Generic fallback
else -> {
showError("Verification error: ${error.message}")
}
}
}

Cancellation Handling

onCancel = {
// User pressed back or cancelled at any step
Log.d("Verification", "User cancelled verification")

// Navigate back or show appropriate UI
navigateBack()
}

Step Change Callback (Optional)

For tracking verification progress, use the onStepChange callback in NafsiConfig:

val config = NafsiConfig(
token = jwtToken,
onStepChange = { step ->
when (step) {
VerificationStep.LANDING -> Log.d("Step", "On landing screen")
VerificationStep.DOCUMENT_SELECTION -> Log.d("Step", "Selecting document")
VerificationStep.ID_FRONT -> Log.d("Step", "Capturing ID front")
VerificationStep.ID_BACK -> Log.d("Step", "Capturing ID back")
VerificationStep.SELFIE -> Log.d("Step", "Taking selfie")
VerificationStep.REVIEW -> Log.d("Step", "Reviewing images")
VerificationStep.PROCESSING -> Log.d("Step", "Processing verification")
VerificationStep.SUCCESS -> Log.d("Step", "Verification successful")
VerificationStep.ERROR -> Log.d("Step", "Verification failed")
else -> { }
}
}
)

VerificationResult Reference

The VerificationResult object contains comprehensive verification data:

FieldTypeDescription
verificationIdString?Unique identifier for this verification
statusString?Result status: "success" or "fail"
confidenceScoreDouble?Overall confidence score (0.0 - 1.0)
extractedDataExtractedData?Data extracted from ID document
photoComparisonPhotoComparisonResult?Face comparison between selfie and ID photo
faceMatchFaceMatchResult?Alternative face matching result
livenessLivenessResult?Liveness detection result
ageVerificationAgeVerificationResult?Age verification result
selfieUrlString?URL of the uploaded selfie image
errorString?Error message if verification failed
errorCodeString?Error code for programmatic handling

ExtractedData Fields

FieldTypeDescription
fullNameString?Full name from ID
firstNameString?First name
lastNameString?Last name
middleNameString?Middle name
idNumberString?ID number
nationalIdNumberString?National ID number
dateOfBirthString?Date of birth
placeOfBirthString?Place of birth
genderString?Gender
nationalityString?Nationality
expiryDateString?Document expiry date
issueDateString?Document issue date
documentNumberString?Document number

PhotoComparisonResult Fields

FieldTypeDescription
matchBoolean?Whether faces match
confidenceDouble?Match confidence (0.0 - 1.0)
scoreDouble?Raw match score

LivenessResult Fields

FieldTypeDescription
isLiveBooleanWhether the selfie is of a live person
confidenceDoubleLiveness confidence (0.0 - 1.0)

AgeVerificationResult Fields

FieldTypeDescription
ageIntCalculated age
isAdultBooleanWhether person is 18+
dateOfBirthString?Date of birth used for calculation

Helper Methods

VerificationResult provides helper methods for common checks:

// Check if verification was successful
if (result.isSuccess()) {
// Verification passed
}

// Check if there was an error
if (result.isError()) {
val errorDescription = result.getErrorDescription()
}

Error Codes Reference

Error TypeError CodeDescription
InvalidJWTINVALID_JWTJWT token is malformed
JWTExpiredJWT_EXPIREDJWT token has expired
JWTDecodeFailedJWT_DECODE_ERRORFailed to decode JWT
NetworkErrorNETWORK_ERRORNetwork connectivity issue
ApiErrorAPI_ERRORAPI returned an error
TimeoutErrorTIMEOUT_ERRORRequest timed out
TokenRefreshFailedTOKEN_REFRESH_FAILEDFailed to refresh token
CameraPermissionDeniedCAMERA_PERMISSION_DENIEDCamera permission denied
CameraNotAvailableCAMERA_NOT_AVAILABLECamera not available
CameraErrorCAMERA_ERRORCamera error
ImageProcessingErrorIMAGE_PROCESSING_ERRORImage processing failed
VerificationFailedVERIFICATION_FAILEDVerification failed
VerificationRejectedVERIFICATION_REJECTEDVerification rejected
WorkflowFetchFailedWORKFLOW_FETCH_FAILEDFailed to fetch workflow
UnknownErrorUNKNOWN_ERRORUnknown error

Support

License

Copyright 2026 Nafsi AI. All rights reserved.