Face Authentication provides a secure 2FA solution using facial recognition. The complete process involves three main steps:
Webhooks are automatically sent to your configured URLs after successful face registration, allowing you to track enrollment status in real-time.
This API generates a unique URL for face registration. Before storing face images, you need to get a registration URL which will be used to identify the face record. This URL is used in the face registration process.
/v2/verification/get-2fa-urlThe request should be sent as application/json with the following fields:
curl -X POST "https://your-domain.com/v2/verification/get-2fa-url" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"integrationId": "642bbcd107fb985259ac6e66",
"uniqueIdentifier": "user-123"
}'
{
"message": "Face registration URL generated successfully",
"data": {
"success": true,
"message": "Face registration URL generated successfully",
"url": "https://face-auth-domain.com/695f628cc62bc4cfa5422399",
"newFace": {
"_id": "695f628cc62bc4cfa5422399",
"integrationId": "642bbcd107fb985259ac6e66",
"faceId": "user-123",
"images": [],
"enabled": true
}
},
"errors": null
}
{
"message": "Face already registered",
"data": {
"success": false,
"message": "Face already registered",
"face": {
"_id": "695f628cc62bc4cfa5422399",
"integrationId": "642bbcd107fb985259ac6e66",
"faceId": "user-123",
"images": [
{ "url": "verification/.../2fa-smile/...jpg", "_id": "6969df39edabc4e2e7f96650" },
{ "url": "verification/.../2fa-eyeblink/...jpg", "_id": "6969df39edabc4e2e7f96651" },
{ "url": "verification/.../2fa-left/...jpg", "_id": "6969df39edabc4e2e7f96652" },
{ "url": "verification/.../2fa-right/...jpg", "_id": "6969df39edabc4e2e7f96653" }
],
"enabled": true
}
},
"errors": null
}
In case of errors, the API will return appropriate HTTP status codes:
{
"statusCode": 400,
"message": "integrationId and uniqueIdentifier are required",
"error": "Bad Request"
}
newFace._id as faceId in "Store Images Against Face" APIAfter successfully storing images, if webhook URLs are configured in your integration settings, a webhook will be sent to your configured webhook URLs.
The webhook will be sent with the following payload structure:
{
"result": {
"_id": "695f628bc62bc4cfa5422397",
"integrationId": "63f606216282e1447689c43d",
"faceId": "b8ed3f0f-d05f-4c4b-bbd0-4ea5b6fd2ef8",
"images": [
{ "url": "verification/.../2fa-smile/...jpg", "_id": "6968b726a51065f46eed40eb" },
{ "url": "verification/.../2fa-eyeblink/...jpg", "_id": "6968b726a51065f46eed40ec" },
{ "url": "verification/.../2fa-left/...jpg", "_id": "6968b726a51065f46eed40ed" },
{ "url": "verification/.../2fa-right/...jpg", "_id": "6968b726a51065f46eed40ee" }
],
"enabled": true
},
"status": "images_stored",
"timestamp": "2026-01-15T09:45:10.631Z"
}
result.faceId can be used to track which user's face was registeredresult._id if you need to reference this face record in future API callsIn case of errors, the API will return appropriate HTTP status codes:
{
"statusCode": 400,
"message": "Face not found",
"error": "Bad Request"
}
This API verifies the userβs face during 2FA login by comparing a live captured image against previously enrolled face images. Successful verification authenticates the user.
{
"message": "Face verified successfully",
"data": {
"success": true,
"verified": true,
"similarity": 100,
"faceId": "43f7e54d-d758-46ff-971b-216e6ec7059c",
"message": "Face verified successfully"
},
"errors": null
}
In case of errors, the API will return appropriate HTTP status codes:
{
"statusCode": 400,
"message": "Face not enrolled for this user",
"error": "Bad Request"
}
Complete face authentication flow for third party integration:
To send data from your application use the following code to encrypt data before sending to the external api which you can checkout on swagger. Following vaiables must be present in encryptedData.
const data = JSON.stringify({"first_name": "John", "last_name": "Doe", "email": "a@a.com", "unique_identifier": "xxxxxxx-xxxxxxxxx-xxxx"})
function encrptData(data: string){
const algorithm = "aes-256-gcm";
const publicKey = "2eaf548c6022de5a3293fbfdd595afad04f750d9cc861d9c723229bb34fa679f".substring(0, 24);
const privateKey = Buffer.from("2eaf548c6022de5a3293fbfdd595afad04f750d9cc861d9c723229bb34fa679f", 'hex');
const cipher = crypto.createCipheriv(algorithm, privateKey, publicKey);
let enc = cipher.update(data, 'utf8', 'hex');
enc += cipher.final('hex');
return enc;
}
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class Main {
public static void main(String[] args) throws Exception {
String encryptedData = encryptData("test", "testing", "admin@user.com", "1");
System.out.println(encryptedData);
}
public static String encryptData(String firstName, String lastName, String email, String Id) throws Exception {
String data = "{\"first_name\":\""+firstName+"\",\"last_name\":\""+lastName+"\",\"email\":\""+email+"\",\"unique_identifier\":\""+Id+"\"}";
String algorithm = "AES/GCM/NoPadding";
String publicKey = "2eaf548c6022de5a3293fbfdd595afad04f750d9cc861d9c723229bb34fa679f".substring(0, 24);
String privateKeyHex = "2eaf548c6022de5a3293fbfdd595afad04f750d9cc861d9c723229bb34fa679f";
byte[] publicKeyBytes = publicKey.getBytes(StandardCharsets.UTF_8);
byte[] privateKeyBytes = hexStringToByteArray(privateKeyHex);
SecretKey secretKey = new SecretKeySpec(privateKeyBytes, "AES");
Cipher cipher = Cipher.getInstance(algorithm);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, publicKeyBytes);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec);
byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return bytesToHex(encryptedBytes).substring(0, 184);
}
public static byte[] hexStringToByteArray(String hexString) {
int length = hexString.length();
byte[] byteArray = new byte[length / 2];
for (int i = 0; i < length; i += 2) {
byteArray[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) +
Character.digit(hexString.charAt(i + 1), 16));
}
return byteArray;
}
public static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte aByte : bytes) {
result.append(String.format("%02x", aByte));
}
return result.toString();
}
}
To get verification details from your application use the following code to encrypt data before sending to the external api which you can checkout on swagger. Following vaiables must be present in encryptedData.
const data = JSON.stringify({"verification_id": "64c262e079e46e716f303690"})
function encrptData(data: string){
const algorithm = "aes-256-gcm";
const publicKey = "f9b1e5aa289929cefdf65449ebfa81bcd7bfad488b14a39c5381bb208128ed20".substring(0, 24);
const privateKey = Buffer.from("c2c30795e9d5e2604c09e869ee9bd5d146b42cb235aaa65fbcb9043aae305efe", 'hex');
const cipher = crypto.createCipheriv(algorithm, privateKey, publicKey);
let enc = cipher.update(data, 'utf8', 'hex');
enc += cipher.final('hex');
return enc;
}
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class Main {
public static void main(String[] args) throws Exception {
String encryptedData = encryptData("test", "testing", "admin@user.com", "1");
System.out.println(encryptedData);
}
public static String encryptData(String verificationId) throws Exception {
String data = "{\"verification_id\":\""+verificationId+"\"}";
String algorithm = "AES/GCM/NoPadding";
String publicKey = "f9b1e5aa289929cefdf65449ebfa81bcd7bfad488b14a39c5381bb208128ed20".substring(0, 24);
String privateKeyHex = "c2c30795e9d5e2604c09e869ee9bd5d146b42cb235aaa65fbcb9043aae305efe";
byte[] publicKeyBytes = publicKey.getBytes(StandardCharsets.UTF_8);
byte[] privateKeyBytes = hexStringToByteArray(privateKeyHex);
SecretKey secretKey = new SecretKeySpec(privateKeyBytes, "AES");
Cipher cipher = Cipher.getInstance(algorithm);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, publicKeyBytes);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec);
byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return bytesToHex(encryptedBytes).substring(0, 184);
}
public static byte[] hexStringToByteArray(String hexString) {
int length = hexString.length();
byte[] byteArray = new byte[length / 2];
for (int i = 0; i < length; i += 2) {
byteArray[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) +
Character.digit(hexString.charAt(i + 1), 16));
}
return byteArray;
}
public static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte aByte : bytes) {
result.append(String.format("%02x", aByte));
}
return result.toString();
}
}
For callback url you can following parameters in the admin panel.
Callback URL should look something like the following
http://example.com/verification-completed?user_id=[user_id]&status=[status]&email=[email]
For webhooks you first need to add url where you want the payload inside the webhook section of integeration in admin panel. There are three types of webhooks
Following is the payload you will get with all the webhook types. "uniqueIdentifier" will have the "unique_identifier" sent during the creation of verificaiton
id: string, //verification id to be saved
integrationId: string,
status: VerificationStatusEnum,
uniqueIdentifier: string,
documentType?: VerificationDocumentEnum | null,
requestUrl: string,
hasError?: boolean,
phoneNumberVerified: boolean,
verifiedPhoneNumber: string,
documentBack: {url: string},
documentFront: {url: string},
verificationDetails: {
firstName: string,
lastName: string,
dateOfBirth: string | null,
gender: string | null,
idNumber: string | null,
nationality: string | null,
placeOfBirth: string | null,
name: string | null,
surname: string | null,
expiration_date: string | null,
address: string | null,
country: string | null,
}
reason: string | null,
Not Started,
Started,
Submitted,
Expired,
Abandoned,
Declined,
Approved,
Under Review,
Passport,
License,
Address Permit,
Proof Address,
Following is the example for how to implement the webhooks
import {
Body,
Controller,
Inject,
Post,
Logger,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ClientRMQ } from '@nestjs/microservices';
import { MESSAGE_PATTERNS, SERVICES } from '../constants';
import { firstValueFrom } from 'rxjs';
import { ApiExcludeController } from '@nestjs/swagger';
// Enums
enum VerificationStatus {
Not_Started = "Not Started",
Started = "Started",
Submitted = "Submitted",
Expired = "Expired",
Abandoned = "Abandoned",
Declined = "Declined",
Approved = "Approved",
UnderReview = "Under Review",
}
export enum VerificationDocument {
Passport = "Passport",
License = "License",
Address_Permit = "Address Permit",
Proof_Address = "Proof Address",
}
// Webhook Request Format
export class WebhookRequestDto {
id: string, //verification id to be saved
integrationId: string,
status: VerificationStatusEnum,
uniqueIdentifier: string,
documentType?: VerificationDocumentEnum | null,
requestUrl: string,
hasError?: boolean,
phoneNumberVerified: boolean,
verifiedPhoneNumber: string,
documentBack: {url: string},
documentFront: {url: string},
verificationDetails: {
firstName: string,
lastName: string,
dateOfBirth: string | null,
gender: string | null,
idNumber: string | null,
nationality: string | null,
placeOfBirth: string | null,
name: string | null,
surname: string | null,
expiration_date: string | null,
address: string | null,
country: string | null,
}
reason: string | null,
}
const {
ADMIN_USER: { ADMIN_CONFIRM_SIGNUP },
} = MESSAGE_PATTERNS.USER_ACCOUNT;
const {
USER: { UPDATE_USER },
} = MESSAGE_PATTERNS.USER_PROFILE;
@ApiExcludeController()
@Controller('webhooks')
export class WebhookController {
private readonly logger = new Logger(WebhookController.name);
constructor(
@Inject(SERVICES.USER_ACCOUNT) private authClient: ClientRMQ,
@Inject(SERVICES.USER_PROFILE) private userClient: ClientRMQ,
) {}
@Post('status')
@HttpCode(HttpStatus.OK)
async status(@Body() dto: WebhookRequestDto) {
// Signed up for user as documents are submitted which means that it was a verified email
if (dto.status === VerificationStatus.Submitted) {
await firstValueFrom(
this.authClient.send(ADMIN_CONFIRM_SIGNUP, {
userId: dto.uniqueIdentifier,
}),
);
}
await firstValueFrom(
this.userClient.send(UPDATE_USER, {
userId: dto.uniqueIdentifier,
idVerificationStatus: dto.status,
// Email verified and active
...(dto.status === VerificationStatus.Submitted && {
emailVerified: new Date(),
isActive: true,
}),
}),
);
return {
data: null,
message: 'Integration client has received the status.',
errors: null,
};
}
@Post('decision')
@HttpCode(HttpStatus.OK)
async decision(@Body() dto: WebhookRequestDto) {
// User Syncing
await firstValueFrom(
this.userClient.send(UPDATE_USER, {
userId: dto.uniqueIdentifier,
idVerified: dto.status === VerificationStatus.Approved,
idVerificationStatus: dto.status,
}),
);
return {
data: null,
message: `Integration client has received the decision.`,
errors: null,
};
}
@Post('proof-of-address')
@HttpCode(HttpStatus.OK)
async proofOfAddress(@Body() dto: WebhookRequestDto) {
// Scenario goes here
return {
data: null,
message: `Integration client has received the decision.`,
errors: null,
};
}
}