ig-logo

Identity Gram External API

Face Authentication API

Face Authentication provides a secure 2FA solution using facial recognition. The complete process involves three main steps:

  1. Get Registration URL: Get a unique URL and faceId for face registration
  2. Face Verification: Verify user's identity during login by comparing live image with registered face

Webhooks are automatically sent to your configured URLs after successful face registration, allowing you to track enrollment status in real-time.

Get 2FA Face Registration URL

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.

πŸ“‘ API Endpoint

  • Method: POST
  • URL: /v2/verification/get-2fa-url
  • Content-Type: application/json

πŸ“€ Request Format

The request should be sent as application/json with the following fields:

  • publicKey (string, required): Your publicKey
  • uniqueIdentifier (string, required): Unique identifier of the user (same identifier used for face verification)

πŸ“‹ Request Example (cURL)

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"
  }'

1️⃣ Response Example (Success)

{
                "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
              }

2️⃣ Response Example (Face Already Registered)

{
                "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
              }

❌ Error Responses

In case of errors, the API will return appropriate HTTP status codes:

  • 400 Bad Request: Missing publicKey or uniqueIdentifier, or FACE_AUTH_URL not configured
  • 401 Unauthorized: Missing or invalid authentication token
  • 500 Internal Server Error: Server-side error

πŸ“‹ Error Response Example

{
                "statusCode": 400,
                "message": "integrationId and uniqueIdentifier are required",
                "error": "Bad Request"
              }

πŸ’‘ Usage Flow

  1. Step 1: Call "Get 2FA Face Registration URL" API to get the registration URL and faceId
  2. Step 2: Use the returned newFace._id as faceId in "Store Images Against Face" API
  3. Step 3: Upload the 4 face images (smile, eyeblink, left, right) using the faceId

πŸ”” Webhook Configuration

After successfully storing images, if webhook URLs are configured in your integration settings, a webhook will be sent to your configured webhook URLs.

πŸ“₯ Webhook Request Details

  • Method: POST
  • URL: Your configured webhook URL (set in admin panel integration settings)
  • Content-Type: application/json
  • When Sent: Immediately after images are successfully stored

πŸ“¦ Webhook Payload Structure

The webhook will be sent with the following payload structure:

  • result (object): Contains the face registration details
    • _id (string): Unique identifier of the face record
    • integrationId (string): Your integration ID
    • faceId (string): Unique face identifier (same as provided in faceId parameter)
    • images (array): Array of enrolled face images
      • Each image object contains:
        • url (string): URL path to the stored image
        • type (string, optional): Image type (e.g., "2fa-smile", "2fa-eyeblink", "2fa-left", "2fa-right")
        • _id (string, optional): Unique identifier for the image record
    • enabled (boolean): Whether the face authentication is enabled (default: true)
  • status (string): Webhook status indicator - always "images_stored" for this event
  • timestamp (string): ISO 8601 timestamp when the webhook was sent

3️⃣ Webhook Payload Example

{
                "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"
              }

πŸ“ Webhook Handling Notes

  • The webhook is sent as a POST request to your configured webhook URL
  • You should return a 200 OK response to acknowledge receipt
  • If your endpoint is unavailable or returns an error, the webhook delivery may be retried (implementation dependent)
  • The result.faceId can be used to track which user's face was registered
  • Store the result._id if you need to reference this face record in future API calls

❌ Error Responses

In case of errors, the API will return appropriate HTTP status codes:

  • 400 Bad Request: Invalid faceId, missing files, or invalid file format
  • 401 Unauthorized: Missing or invalid authentication token
  • 404 Not Found: Face record not found for the provided faceId
  • 500 Internal Server Error: Server-side error

πŸ“‹ Error Response Example

{
                "statusCode": 400,
                "message": "Face not found",
                "error": "Bad Request"
              }

Verify Face 2FA

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.

1️⃣ Parameters (Purpose)

  • integrationId: string - Integration ID of the system.
  • uniqueIdentifier: string - Unique identifier of the user.
  • image: image file (jpeg/png) - Live capture image from the user for verification.

2️⃣ Response Example

{
                "message": "Face verified successfully",
                "data": {
                  "success": true,
                  "verified": true,
                  "similarity": 100,
                  "faceId": "43f7e54d-d758-46ff-971b-216e6ec7059c",
                  "message": "Face verified successfully"
                },
                "errors": null
              }

❌ Error Responses

In case of errors, the API will return appropriate HTTP status codes:

  • 400 Bad Request: Missing image file, invalid file format, or face not found
  • 401 Unauthorized: Missing or invalid authentication token
  • 404 Not Found: User face not enrolled or integration not found
  • 500 Internal Server Error: Server-side error

πŸ“‹ Error Response Example

{
                "statusCode": 400,
                "message": "Face not enrolled for this user",
                "error": "Bad Request"
              }

πŸ’‘ Face Authentication Flow

Complete face authentication flow for third party integration:

  1. Step 1 - Get Registration URL: Use "Get 2FA Face Registration URL" API to get a unique faceId for the user
  2. Step 2 - Face Registration: Use "Store Images Against Face" API with the faceId to register user's face with 4 images (smile, eyeblink, left, right)
  3. Step 3 - Receive Webhook: After successful registration, you will receive a webhook at your configured URL with the face registration details
  4. Step 4 - Face Verification: During login/2FA, use "Verify Face 2FA" API with a live captured image to verify the user
  5. Step 5 - Handle Response: Based on the verification response (verified: true/false and similarity score), authenticate or reject the user

Add New Verification

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.

  1. first_name
  2. last_name
  3. unique_identifier


    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;
    }

Java Implementation for encryption


    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();
        }
    }

*Public and Private keys can be retrived from the admin panel inside integeration

Get Verification

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.

  1. verification_id


    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;
    }

Java Implementation for encryption


    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();
        }
    }

*Public and Private keys can be retrived from the admin panel inside integeration

Callback URL in Admin Panel

For callback url you can following parameters in the admin panel.

  1. user_id: will containg the unique_identifier sent when requesting in api creating verification.
  2. status: will contain the current status of the verification
  3. email: will contain email if it was provided at the time of creation

Callback URL should look something like the following

    
    http://example.com/verification-completed?user_id=[user_id]&status=[status]&email=[email]

Webhooks

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

  1. Status Webhook: Will hit any time the status is changed of verification.
  2. Decision Webhook: Will hit only if decision is made either approved or rejected.
  3. Proof Address Webhook: Will hit for the decision if document type is proof of address.

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,

VerificationStatusEnum

    Not Started,
    Started,
    Submitted,
    Expired,
    Abandoned,
    Declined,
    Approved,
    Under Review,

VerificationDocumentEnum

    Passport,
    License,
    Address Permit,
    Proof Address,

Webhooks Implementation Example

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,
                };
            }
        }