Skip to main content

Quickstart

This walks through a complete end-to-end check: your backend mints a session token, a Flutter app consumes it, the user completes a liveness challenge, and your backend reads the result. Expect to spend about ten minutes.

Prerequisites​

  • Node.js 18+ for the server stub below. Any language can make the two HTTP calls it shows.
  • Flutter 3.13+ with a connected device or running emulator.
  • A drukverify tenant. If you don't have one yet, request access. The dashboard gives you dv_sk_test_… and dv_sk_live_… secret keys and dv_pk_test_… and dv_pk_live_… public keys.

1. Mint a session on your server​

The secret key (dv_sk_…) stays on your server; you never ship it to a mobile app. The server's job is to mint short-lived session tokens (dv_tok_… JWTs, 30 minutes by default, one hour at most) that the mobile app uses for one verification session.

The call is one request:

curl -X POST https://api.drukverify.com/v1/sessions \
-H "Authorization: Bearer $EKYC_SECRET_KEY" \
-H "Drukverify-Version: 2026-05-12" \
-H "Content-Type: application/json" \
-d '{"customer_ref":"demo","ttl_seconds":1800}'
# → {"token":"dv_tok_test_eyJhbGciOi...","jti":"…","expires_at":"…","scopes":[],"mode":"test"}

A minimal Node.js server that does it for your app (server.mjs):

import express from 'express';

const GATEWAY = 'https://api.drukverify.com';
const app = express();

app.get('/session', async (req, res) => {
const customerRef = req.query.user; // your own user id
if (!customerRef) return res.status(400).send('user query param required');
const r = await fetch(`${GATEWAY}/v1/sessions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.EKYC_SECRET_KEY}`, // dv_sk_test_… in development
'Drukverify-Version': '2026-05-12',
'Content-Type': 'application/json',
},
body: JSON.stringify({ customer_ref: String(customerRef), ttl_seconds: 1800 }),
});
if (!r.ok) return res.status(502).send('could not mint session');
const session = await r.json();
res.json({ token: session.token, expires_at: session.expires_at });
});

app.listen(9090, () => console.log('listening on :9090'));

Run it and check:

EKYC_SECRET_KEY=dv_sk_test_… node server.mjs
curl 'http://localhost:9090/session?user=demo'
# → {"token":"dv_tok_test_eyJhbGciOi...","expires_at":"…"}

2. Wire the Flutter app​

Add the SDK to pubspec.yaml. It ships from a private GitHub repository; ask your integration contact for access if you don't already have it.

dependencies:
drukverify_flutter:
git:
url: git@github.com:DrukVerify/drukverify-flutter.git
ref: v2.5.0
drukverify_flutter_generated:
git:
url: git@github.com:DrukVerify/drukverify-flutter.git
ref: v2.5.0
path: lib/src/_generated

In your app's startup code:

import 'package:dio/dio.dart';
import 'package:drukverify_flutter/drukverify_flutter.dart';

final client = EkycClient(
config: EkycConfig(
host: 'https://api.drukverify.com',
apiKey: 'dv_pk_test_…', // your public tenant key
tokenProvider: () async {
final res = await Dio().get<Map<String, dynamic>>(
'http://10.0.2.2:9090/session', // Android emulator → host
queryParameters: {'user': 'demo'},
);
return res.data!['token'] as String;
},
),
);

tokenProvider is called lazily: the first authenticated request triggers a fetch from your backend. The SDK caches the token and calls the provider again on a 401 expired_token.

3. Run a liveness challenge​

In a screen widget:

import 'package:drukverify_flutter/drukverify_flutter.dart';

EkycLivenessWidget(
client: client,
onResult: (LivenessResult result) {
if (result.error != null) {
print('liveness error: ${result.error}');
return;
}
print('isLive=${result.isLive} '
'lc_id=${result.serverResponse?['lc_id']}');
},
);

The widget asks the platform for a challenge (blink, turn left or turn right), guides the user through it with on-device face detection, captures frames from the camera stream while they perform it, and uploads the sequence to /v1/liveness/check. The result (is_live, score, reason, lc_id) is the server's verdict; on-device detection only drives the prompts. Behaviour is described on the widget page, and the capture rules the server expects are in Liveness checks.

4. Read the result on the server​

result.serverResponse['lc_id'] is a stable identifier (lc_<32 hex>). Your server can fetch the check later with GET /v1/liveness/checks/:id for audit or risk review, or subscribe to the liveness.check.completed webhook.

For a flow that binds the document scan, the liveness check and a server-side face match into one decision, start from Verification sessions instead of POST /v1/sessions.

What's next​