KYC Docs Get credentials
Docs › Blink SDK › Flutter

Flutter

Install blink_kyc, create a session token on your backend, let the SDK run the document and liveness capture, then read the verified result from your backend. This guide covers both sides: the Flutter app and the server it talks to.

package blink_kyc ^1.0.13 API https://kyc-api.blink-pay.net Flutter 3.32+ · Dart 3.8+ iOS 15.5+ · Android API 24+

00How it works

Three parties, three calls. Your client secret lives only on your backend; the phone only ever holds a short-lived session token; and the result you act on is the one your backend reads from Blink.

1 · Your backend

Create a session

POST /api/blink/session/create with your client key and secret. Returns sessionId and sessionToken.

2 · Your Flutter app

Run the capture

Pass the sessionToken to BlinkKyc.present. The SDK shows the camera screens and uploads to Blink.

3 · Your backend

Read the result

GET /api/blink/session/{sessionId}/result. Act on VERIFIED, REJECTED or REVIEW.

Never put the client secret in the app. Anything shipped in a Flutter build can be extracted. The app asks your backend for a session token, and your backend asks Blink.

01Install the package

blink_kyc is served from the Blink package repository, not pub.dev. Add it with a hosted source:

pubspec.yamldependencies
dependencies:
  flutter:
    sdk: flutter
  blink_kyc:
    hosted: https://blink-pay.net/pub
    version: ^1.0.13
  http: ^1.2.0   # to call your own backend
terminalshell
flutter pub get

Import it wherever you start verification:

lib/kyc.dartdart
import 'package:blink_kyc/blink_kyc.dart';

02Android & iOS setup

blink_kyc is a thin bridge over the native Blink Android and iOS SDKs: the native capture screens own the camera, the on-device checks and the upload.

Android

The plugin depends on the native Blink Android SDK, served from the Blink Maven repository. Add the repository to your app's Gradle build, and keep the minimum SDK at 24 or higher:

android/build.gradle.ktskotlin
allprojects {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://blink-pay.net/maven/releases") }   // Blink SDK repository
    }
}
android/app/build.gradle.ktskotlin
android {
    defaultConfig {
        minSdk = 24   // required by the Blink Android SDK
    }
}

The camera and internet permissions and the capture screens are merged in from the SDK; a plain FlutterActivity works, and the screens ask for the camera themselves. Build with JDK 17 or 21.

iOS

At pod install the plugin downloads the prebuilt Blink iOS SDK from the Blink artifact site. It needs iOS 15.5, and pods linked statically, because the SDK and TensorFlow Lite are static binaries:

ios/Podfileruby
platform :ios, '15.5'

target 'Runner' do
  use_frameworks! :linkage => :static
  # … the rest of Flutter's generated Podfile
end

Add the camera usage description to ios/Runner/Info.plist:

ios/Runner/Info.plistxml
<key>NSCameraUsageDescription</key>
<string>We use the camera to photograph your document and confirm your identity.</string>

Passport chip reading is off in 1.0.13. The native screens capture the document photo and the selfie; no NFC capability or NFC keys are needed.

03Backend: create a session

Expose an endpoint on your backend that your app calls when the user starts verification. It authenticates your user, calls Blink with your client credentials, and returns only the session token and id.

The Blink call

POST https://kyc-api.blink-pay.net/api/blink/session/createhttp
curl -sX POST https://kyc-api.blink-pay.net/api/blink/session/create \
  -H 'content-type: application/json' \
  -d '{
        "clientKey":    "bkyc_live_…",
        "clientSecret": "bksec_…",
        "purpose":      "FULL_CAPTURE",
        "userRef":      "customer-8841"
      }'

→ 200
{
  "sessionId":    "[SESSION_ID]",
  "sessionToken": "bkyc_sess_…",
  "expiresAt":    "2026-09-15T18:42:10Z",
  "purpose":      "FULL_CAPTURE"
}
FieldRequiredMeaning
clientKeyyesYour client key. Server-side only.
clientSecretyesYour client secret. Server-side only — never in the app.
purposenoFULL_CAPTURE (default: document + liveness + face match), DOCUMENT_VERIFICATION, LIVENESS or FACE_MATCH.
userRefnoYour own opaque id for the end user. Don't send personal data here.

Store sessionId against your user — you need it to read the result. Sessions are short-lived: create one when the user taps Verify, not ahead of time.

Example: Node.js (Express)

server.jsjavascript
import express from 'express';

const app = express();
const BLINK = 'https://kyc-api.blink-pay.net';

// Called by your Flutter app. requireUser = your own auth middleware.
app.post('/kyc/sessions', requireUser, async (req, res) => {
  const r = await fetch(`${BLINK}/api/blink/session/create`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      clientKey: process.env.BLINK_CLIENT_KEY,
      clientSecret: process.env.BLINK_CLIENT_SECRET,
      purpose: 'FULL_CAPTURE',
      userRef: req.user.id,
    }),
  });
  if (!r.ok) return res.status(502).json({ error: 'kyc_unavailable' });

  const { sessionId, sessionToken, expiresAt } = await r.json();
  await db.kycSessions.insert({ userId: req.user.id, sessionId, status: 'CREATED' });

  // Only the token and id go to the phone. Never the key or secret.
  res.json({ sessionId, sessionToken, expiresAt });
});

Example: Dart (shelf)

bin/server.dartdart
import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart' as http;
import 'package:shelf/shelf.dart';

const blink = 'https://kyc-api.blink-pay.net';

Future<Response> createKycSession(Request request, String userId) async {
  final r = await http.post(
    Uri.parse('$blink/api/blink/session/create'),
    headers: {'content-type': 'application/json'},
    body: jsonEncode({
      'clientKey': Platform.environment['BLINK_CLIENT_KEY'],
      'clientSecret': Platform.environment['BLINK_CLIENT_SECRET'],
      'purpose': 'FULL_CAPTURE',
      'userRef': userId,
    }),
  );
  if (r.statusCode != 200) return Response(502);

  final body = jsonDecode(r.body) as Map<String, dynamic>;
  // Save body['sessionId'] against userId before replying.
  return Response.ok(
    jsonEncode({'sessionId': body['sessionId'], 'sessionToken': body['sessionToken']}),
    headers: {'content-type': 'application/json'},
  );
}

04App: run the capture

Ask your backend for a session, hand the token to BlinkKyc.present, and the native capture screens take over: document detection, auto-capture, on-device MRZ reading and liveness.

lib/kyc.dartdart
import 'dart:convert';

import 'package:blink_kyc/blink_kyc.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;

Future<void> startVerification(String userJwt) async {
  // 1. Your backend creates the Blink session (section 03).
  final res = await http.post(
    Uri.parse('https://api.yourbank.com/kyc/sessions'),
    headers: {'authorization': 'Bearer $userJwt'},
  );
  final session = jsonDecode(res.body) as Map<String, dynamic>;

  try {
    // 2. The native SDK runs document + liveness capture and uploads to Blink.
    final outcome = await BlinkKyc.present(BlinkConfig(
      sessionToken: session['sessionToken'] as String,
      issuingCountry: 'SDN',                        // SDN | ARE; omit to let the session decide
      documentType: BlinkDocumentType.nationalId,   // passport | nationalId; omit to let the session decide
      locale: 'ar',                                 // Blink's built-in Arabic; omit for the device locale
    ));

    // 3. Show progress to the user, but decide nothing here —
    //    ask your backend, which reads the authoritative result (section 05).
    debugPrint('device copy: ${outcome.verdict.name}');   // verified | rejected | review
    await http.post(
      Uri.parse('https://api.yourbank.com/kyc/sessions/${session['sessionId']}/complete'),
      headers: {'authorization': 'Bearer $userJwt'},
    );
  } on BlinkKycException catch (e) {
    // A cancelled journey, a failed step or a session problem. Switch on e.code, not the message.
    if (e.code == 'BLINK_CAPTURE_CANCELLED') return;   // user closed the camera
    debugPrint('${e.code}: ${e.message}');
  }
}

The configuration

FieldMeaning
sessionTokenRequired. The short-lived token your backend got from Blink.
baseUrlThe Blink API. Defaults to https://kyc-api.blink-pay.net.
issuingCountry'SDN' or 'ARE'. Omit to let the session decide.
documentTypeBlinkDocumentType.passport or nationalId. Two-sided cards capture front then back.
locale'ar' for Blink's built-in Arabic, right to left; 'en' for English. Omit for the device locale.
rejectExpiredDocumentsOn by default: an expired document ends the journey with BLINK_DOCUMENT_EXPIRED. false leaves the decision to Blink.
themeA BlinkTheme: colours, sizes, positions and images for the native screens. Omit for the built-in look. See Customise.

BlinkKyc.present resolves with a BlinkSessionResult: verdict (BlinkVerdict.verified, rejected or review) and an optional detail. It throws BlinkKycException with the native code otherwise.

The session's policy wins. The native SDK reads the session's required steps and allowed documents from Blink when it starts. The configuration only fills gaps: the steps come from the session's purpose.

Hosted session links

If your dashboard or backend produces a hosted link (https://kyc-api.blink-pay.net/h/?t=bkyc_sess_…), for example as a QR code, take the session token from its t parameter. Only accept links for the API host you expect:

lib/kyc.dartdart
final link = Uri.parse(scannedLink);
if (link.scheme != 'https' || link.host != 'kyc-api.blink-pay.net') throw const FormatException('Not a Blink link');
final token = link.queryParameters['t'];
if (token == null) throw const FormatException('No session token in the link');

final outcome = await BlinkKyc.present(BlinkConfig(
  sessionToken: token,
  locale: link.queryParameters['lang'] == 'en' ? 'en' : 'ar',   // hosted links default to Arabic
));

05Backend: read the result

This is the only verdict to act on. The value BlinkKyc.present returns on the phone is a convenience copy that a modified app could fake; your backend reads the real one, server to server.

GET https://kyc-api.blink-pay.net/api/blink/session/{sessionId}/resulthttp
curl -s https://kyc-api.blink-pay.net/api/blink/session/[SESSION_ID]/result \
  -H 'X-Blink-Client-Key: bkyc_live_…' \
  -H 'X-Blink-Client-Secret: bksec_…'

→ 200
{
  "sessionId":  "[SESSION_ID]",
  "status":     "COMPLETED",
  "result":     "VERIFIED",
  "resultJson": "{\"sessionId\":\"[SESSION_ID]\",\"purpose\":\"FULL_CAPTURE\",\"result\":\"VERIFIED\",\"faceScore\":0.91,\"detail\":\"…\"}"
}
FieldValuesUse
statusCREATED · IN_PROGRESS · COMPLETED · EXPIRED · SUSPENDEDWhere the session is. Only COMPLETED carries a result.
resultVERIFIED · REJECTED · REVIEW · nullThe decision. null until the session completes. Route REVIEW to a person.
resultJsonJSON encoded as a string, or nullA record for your logs and support screens: sessionId, purpose, result, a neutral detail, and faceScore when a face match ran. Decode it as a second step. Never branch on it — use result.

The endpoint returns 404 with BLINK_RESULT_NOT_FOUND for an unknown session, a session belonging to another client, or wrong credentials — deliberately indistinguishable. It is idempotent, so it is safe to call again.

Example: finish the verification (Node.js)

server.jsjavascript
// Called by the app after BlinkKyc.present returns. Also poll it from a job for users who close the app.
app.post('/kyc/sessions/:sessionId/complete', requireUser, async (req, res) => {
  const row = await db.kycSessions.find({ sessionId: req.params.sessionId, userId: req.user.id });
  if (!row) return res.sendStatus(404);

  const r = await fetch(`${BLINK}/api/blink/session/${row.sessionId}/result`, {
    headers: {
      'X-Blink-Client-Key': process.env.BLINK_CLIENT_KEY,
      'X-Blink-Client-Secret': process.env.BLINK_CLIENT_SECRET,
    },
  });
  if (!r.ok) return res.status(502).json({ error: 'kyc_unavailable' });

  const { status, result, resultJson } = await r.json();
  const record = resultJson ? JSON.parse(resultJson) : null;   // for logs / support
  await db.kycSessions.update(row.id, { status, result, detail: record?.detail });

  if (status !== 'COMPLETED') return res.json({ state: 'pending' });
  if (result === 'VERIFIED') await db.users.markVerified(req.user.id);
  res.json({ state: result.toLowerCase() });   // verified | rejected | review
});

Example: the same in Dart

bin/server.dartdart
Future<Map<String, dynamic>> fetchBlinkResult(String sessionId) async {
  final r = await http.get(
    Uri.parse('$blink/api/blink/session/$sessionId/result'),
    headers: {
      'X-Blink-Client-Key': Platform.environment['BLINK_CLIENT_KEY']!,
      'X-Blink-Client-Secret': Platform.environment['BLINK_CLIENT_SECRET']!,
    },
  );
  if (r.statusCode == 404) throw StateError('Unknown session or wrong credentials');
  if (r.statusCode != 200) throw StateError('Blink returned ${r.statusCode}');

  final body = jsonDecode(r.body) as Map<String, dynamic>;
  final raw = body['resultJson'] as String?;
  return {
    'status': body['status'],                         // COMPLETED when done
    'result': body['result'],                         // VERIFIED | REJECTED | REVIEW | null
    'record': raw == null ? null : jsonDecode(raw),   // detail, faceScore, purpose
  };
}

06KYC details & document images

The SDK uploads what it captures directly to Blink and does not hand the images back to your app. Your backend downloads the stored document scans and selfies with the Documents API: one call lists them, one returns them as a zip.

Extracted identity fields — name, date of birth, document number — are not returned by the client API. If your compliance process needs them from Blink, talk to us.

The Flutter plugin always runs the built-in native screens, so it cannot hand the camera to your own code. If you need your own capture screens, for example to keep a copy of the image in the app, integrate the native SDKs' headless mode instead (Android, iOS). Blink still issues the challenges, receives the upload and decides.

07Customise

Pass a BlinkTheme to style the native screens. Every field is optional and keeps the built-in look when left out:

lib/kyc.dartdart
final result = await BlinkKyc.present(BlinkConfig(
  sessionToken: sessionToken,
  theme: const BlinkTheme(
    accent: Color(0xFF15803D),
    brandName: 'Your Bank',
    logo: BlinkImage.asset('assets/bank_logo.png'),
    scrimColor: Color(0xFF0E1E4D),   // the dimmed area outside the document frame
    scrimOpacity: 0.35,              // 0 clear … 1 opaque; 0.6 unless set
    guideSuccessColor: Color(0xFF22C55E),
  ),
));

The fields are the native BlinkTheme's, listed with what each one changes in Customisation. Images are BlinkImage.asset (an asset in your pubspec.yaml), BlinkImage.resource (an Android drawable or iOS asset-catalogue name) or BlinkImage.file. An invalid value throws BlinkKycException with BLINK_CONFIG.

For the language, set locale: 'ar' for Blink's built-in Arabic copy with right-to-left screens. Individual sentences cannot be replaced from Flutter yet.

08Errors

BlinkKyc.present throws BlinkKycException, carrying the native SDK's error code. Switch on code, never on the message text.

CodeRaised byWhat to do
DOCUMENT_UNREADABLEA failed stepAsk the user to retake in better light, then start a new session.
LIVENESS_FAILEDA failed stepOffer another attempt with a new session.
BLINK_SESSION_INVALIDBlink · HTTP 401Token missing, expired or already used. Create a new session.
BLINK_CHALLENGE_INVALIDThe native SDKA step's one-time challenge expired. Retry with a new session.
BLINK_CAPTURE_CANCELLEDThe native SDKThe user closed the camera. Not an error to report.
BLINK_CAMERA_DENIEDThe native SDKExplain why the camera is needed and link to Settings.
BLINK_DOCUMENT_EXPIREDThe native SDKThe document's expiry date has passed. The SDK told the user, sent the photograph so the session records it, and closed when they pressed the button; offer a new attempt with a new session.
BLINK_NETWORK · BLINK_TIMEOUTThe deviceConnectivity. Let the user retry.
BLINK_CONFIGThe pluginA bad configuration value (an unsupported country, a missing token). Fix the call.
BLINK_NO_ACTIVITY · BLINK_NO_UIThe pluginNo Android activity or iOS view controller to show the screens from. Call it from a visible screen.
BLINK_QUOTA_EXCEEDEDBlink · HTTP 429Your plan's quota is used up. Contact Blink.

Blink's HTTP errors all share one shape: {"code": "…", "message": "…", "timestamp": "…"}. The full code list is in the Errors reference.

09Go-live checklist

Credentials for sandbox and production are issued under a Blink KYC integration agreement. Talk to us to get set up.