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_…anddv_sk_live_…secret keys anddv_pk_test_…anddv_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
- Test mode vs live mode — how the platform differentiates by API key
- Authentication — full
sk_/tok_lifecycle - Error handling — error envelope shape and retries