VerifyNow guide
How to Add ID Verification to a Web or Mobile App
A plain-English developer guide to adding a basic South African ID check, passive liveness and risk-based step-up verification to a web or mobile app.

The simplest identity check asks your user for a 13-digit South African ID number. Your app sends it to your own backend, which calls VerifyNow and returns the result your screen needs.
You can add a selfie later in the journey. Passive liveness checks whether the image appears to show a live person. Face Match can then compare that selfie with a reference photo when your product needs stronger evidence.
Create a VerifyNow account and open the API documentation while you work through the examples below.
The app flow
Keep the API key on a server you control. A browser or mobile application can be inspected, which makes any secret shipped inside it recoverable.
Web app or mobile app
|
| ID number or selfie
v
Your backend API
|
| x-api-key + Idempotency-Key
v
VerifyNow API
|
| result for this check
v
Your backend decides the next screen
The same shape works for React, Next.js, native iOS, Android, Flutter and React Native. Only the client-side capture code changes. Your backend owns the VerifyNow calls and the rules that decide when a user needs another check.
Start with basic South African ID verification
Basic ID verification uses:
POST https://www.verifynow.co.za/api/external/verify
The request body contains reportType: "said_verification", the ID number and the mode. The SAID Verification API reference shows the current fields and response.
curl -X POST https://www.verifynow.co.za/api/external/verify \
-H "x-api-key: $VERIFYNOW_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: signup-847-said" \
-d '{
"reportType": "said_verification",
"idNumber": "8001015009087",
"mode": "production"
}'
Use production mode for a live check. During development, send the same payload with "mode": "sandbox".
Put the VerifyNow call in your backend
This Next.js route shows the full boundary. It receives an ID number from the app, checks the format and calls VerifyNow from the server.
// app/api/identity/basic/route.ts
import { NextResponse } from 'next/server';
const VERIFYNOW_URL =
'https://www.verifynow.co.za/api/external/verify';
export async function POST(request: Request) {
const { idNumber, attemptId } = await request.json();
if (!/^\d{13}$/.test(idNumber)) {
return NextResponse.json(
{ error: 'Enter a 13-digit South African ID number.' },
{ status: 400 }
);
}
const response = await fetch(VERIFYNOW_URL, {
method: 'POST',
headers: {
'x-api-key': process.env.VERIFYNOW_API_KEY!,
'Content-Type': 'application/json',
'Idempotency-Key': `${attemptId}:said`
},
body: JSON.stringify({
reportType: 'said_verification',
idNumber,
mode: 'production'
})
});
const data = await response.json();
if (!response.ok) {
return NextResponse.json(
{
error: data.error ?? 'The identity check could not be completed.',
retryable: response.status >= 500
},
{ status: response.status }
);
}
const verification =
data.results?.said_verification?.realTimeResults?.Verification;
return NextResponse.json({
requestId: data.requestId,
status:
data.results?.said_verification?.realTimeResults?.Status,
person: verification
? {
firstNames: verification.Firstnames,
lastName: verification.Lastname,
dateOfBirth: verification.Dob,
citizenship: verification.Citizenship
}
: null
});
}
Store VERIFYNOW_API_KEY in the backend environment. The authentication guide explains how to create and use the key.
Call your backend from a web app
const attemptId = crypto.randomUUID();
const response = await fetch('/api/identity/basic', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
idNumber: form.idNumber,
attemptId
})
});
const result = await response.json();
A mobile app should call the same backend route on your public API domain. The VerifyNow key stays out of the mobile bundle.
const response = await fetch(
'https://api.example.co.za/identity/basic',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
idNumber,
attemptId: createUuid()
})
}
);
Your app can now show a verified state, ask the user to check the number or send the result for review. Keep transport failures in a retry state. A server timeout belongs in the retry queue.
Add passive liveness from one selfie
Passive liveness uses a single face image. The user looks at the camera and takes a normal selfie without a movement challenge. VerifyNow returns Approved, Declined or In Review, along with a score and any warnings.
The result answers one question: does the face appear live? Face Match connects that selfie to identity evidence.
Read the Passive Liveness API reference before choosing your image format and handling the response.
async function runPassiveLiveness(
imageBase64: string,
attemptId: string
) {
const response = await fetch(
'https://www.verifynow.co.za/api/external/passive-liveness',
{
method: 'POST',
headers: {
'x-api-key': process.env.VERIFYNOW_API_KEY!,
'Content-Type': 'application/json',
'Idempotency-Key': `${attemptId}:liveness`
},
body: JSON.stringify({
mode: 'production',
image_base64: imageBase64
})
}
);
const data = await response.json();
if (!response.ok) {
throw new Error(
data.error ?? 'The liveness check could not be completed.'
);
}
return {
requestId: data.requestId,
status: data.results?.liveness?.status,
score: data.results?.liveness?.score,
warnings: data.results?.liveness?.warnings ?? []
};
}
VerifyNow accepts JPG, JPEG, PNG, WEBP and TIFF image data up to 5 MB after decoding. Your capture screen should ask for one face, even lighting and a steady camera. Send the Base64 string with or without its data URL prefix.
An In Review response needs its own screen. Give the user a clear message and send the attempt to the review path used by your team.
Decide when the app should step up
Step-up verification means asking for stronger evidence when the journey carries more risk. Your backend owns that workflow logic and calls the relevant VerifyNow endpoint.
A step-up can start after account recovery, a new device, a high-value action or a verification result marked for review. Set those triggers in one backend policy so the web app and mobile app behave the same way.
type VerificationContext = {
accountRecovery: boolean;
newDevice: boolean;
highValueAction: boolean;
basicStatus: string;
};
function needsFaceStepUp(context: VerificationContext) {
return (
context.accountRecovery ||
context.newDevice ||
context.highValueAction ||
context.basicStatus === 'In Review'
);
}
The policy should return the next action. A result such as face_match_required is easier for every client app to handle than a collection of risk conditions.
Match the selfie to a Home Affairs reference photo
Use bundle: "facematch" when the stronger check should compare the user's selfie with the supported Home Affairs reference photo. The request sends the selfie and ID number.
async function runHomeAffairsFaceMatch(
selfieBase64: string,
idNumber: string,
attemptId: string
) {
const response = await fetch(
'https://www.verifynow.co.za/api/external/facematch',
{
method: 'POST',
headers: {
'x-api-key': process.env.VERIFYNOW_API_KEY!,
'Content-Type': 'application/json',
'Idempotency-Key': `${attemptId}:face`
},
body: JSON.stringify({
bundle: 'facematch',
mode: 'production',
selfie_image_base64: selfieBase64,
id_number: idNumber
})
}
);
const data = await response.json();
if (!response.ok) {
return {
completed: false,
code: data.code,
retryable: response.status >= 500
};
}
return {
completed: true,
requestId: data.requestId,
status: data.results?.face_match?.status,
score: data.results?.face_match?.score,
warnings: data.results?.face_match?.warnings ?? []
};
}
The Home Affairs Face Match reference lists the response shape. A reference_photo_unavailable result means the required reference image was unavailable for that request. Route the customer to another approved method in your journey, such as document capture or manual review.
Passive liveness remains a separate call. Give it another idempotency key, even when both checks use the same selfie.
Use a photo your app already collected
Some products already capture an ID card or passport image. The Face Match API supports two supplied-reference paths.
facematch_standard compares the selfie with the supplied photo. Read the standard Face Match reference for its request fields.
facematch_verified runs the supplied-reference comparison and basic South African ID verification in one VerifyNow call. It needs the selfie, reference image and ID number.
{
"bundle": "facematch_verified",
"mode": "production",
"selfie_image_base64": "data:image/jpeg;base64,/9j/4AAQ...",
"reference_image_base64": "data:image/jpeg;base64,/9j/4BBR...",
"id_number": "8001015009087"
}
This bundle uses your supplied reference image. The Face Match plus ID reference shows the nested face_match and said_verification results.
Use idempotency keys properly
Production requests require an Idempotency-Key. The key protects the user from a duplicate charge when your server retries the same request.
Reuse the same key while retrying the same body. Generate a new key when the body or check changes. A single onboarding attempt could use:
signup-847:said
signup-847:liveness
signup-847:face
signup-847:document
This also makes your logs easier to follow. Read the idempotency documentation for the current conflict and retention behaviour.
Handle results as product states
An API response becomes easier to manage when your backend converts it into a small set of app states.
| Backend state | What the app should do |
|---|---|
verified | Continue the customer journey |
check_input | Ask the user to review the ID number or captured image |
step_up_required | Open the selfie, Face Match or document step |
review_required | Confirm receipt and send the case to the review queue |
retry_later | Keep the attempt open and retry a temporary service failure |
declined_by_policy | Show the outcome written for your own product policy |
Keep the raw VerifyNow response in your protected backend systems according to your retention policy. Send only the fields required by the current screen to the client.
A practical launch checklist
- Put the VerifyNow API key in the backend environment.
- Use production mode for live checks and sandbox mode during integration work.
- Create a separate idempotency key for each VerifyNow call.
- Validate the ID number and image size before making a request.
- Treat
In Reviewas a real product state. - Keep temporary service failures in a retry flow.
- Record the user's purpose, consent or other lawful basis for processing.
- Avoid logging full ID numbers, Base64 images or API keys.
Test each client path on a slow connection. Check what happens when the user closes the app after submitting, retries from another device or uploads an image without a clear face.
Where VerifyNow fits
VerifyNow gives your backend one API surface for the identity checks described here. Start with South African ID Verification, add Passive Liveness where your flow needs a live-presence signal, and use Face Match for a stronger identity link.
The full API integration guide covers authentication, sandbox mode, error handling and production launch details. The endpoint reference remains the source for current request fields.
Start building
Create your VerifyNow account, then use the API documentation to make the first basic ID request. Add liveness and Face Match after the basic flow works from end to end.