Skip to main content

REST API — running a verification from your own backend

This is the contract a third-party application calls to run an identity verification without using the Nafsi SDK. Everything below is the live contract: request shapes are what the gateway parses, and every response example is copied from a real verification, not composed by hand.

Two calls total:

  1. POST /postdata with menu: auth_config — exchange your client credentials for an access token.
  2. POST /upload — send the captures and the verification request in one multipart call, and get the completed verification back in the response.

There is no polling step and no second "fetch result" call. /upload blocks until the verification finishes and returns the whole thing.

Base URL

https://apisv2.windeal.co.ke

All examples use it. Talk to Nafsi before pointing production at any other host.


1. Get an access token

auth_config is a public menu — this call needs no bearer token, only your client credentials.

POST /postdata HTTP/1.1
Host: apisv2.windeal.co.ke
Content-Type: application/json
{
"rpcQueue": "idv-gateway-v2",
"menu": "auth_config",
"method": "generateAccessToken",
"client_id": "88447c1a-4aa7-11f1-b36a-ba8f5d272487",
"client_secret": "<your client secret>"
}

client_id and client_secret come from Console → API Keys (see API Key Management). The secret is shown once, at creation.

The response carries an access_token and a refresh_token. Use the access token as Authorization: Bearer <token> on every subsequent call, and method: "refreshAccessToken" with refresh_token when it expires — the same envelope, so your token handling is one code path.

Keep the secret server-side

client_secret must never reach a browser or a mobile binary. Front-end flows use the SDK, which authenticates with a workflow-scoped refresh token and never sees the secret. See Authentication.


2. Submit a verification

One multipart/form-data request carries both the captures and the request body. The gateway stores each image, rewrites the message with the URLs it minted, and forwards the whole thing to the verification worker — so a single call covers what would otherwise be an upload round per image plus a submit.

POST /upload HTTP/1.1
Host: apisv2.windeal.co.ke
Authorization: Bearer <access_token>
Content-Type: multipart/form-data; boundary=...

Parts

PartTypeRequiredNotes
frontfileyesFront of the document
backfileyesBack of the document. For a passport, send the same image as front
selfiefilenoRequired for face matching and liveness; omit for a document-only check
messagetextyesJSON string, described below

At least two file parts are required. A single file is treated as a legacy one-off upload, not a verification batch.

The multipart field name becomes the slot name, which is what determines the stored filename and the <slot>_url key added to your message. Post a part named front and you get front_url. There is no fixed list — a part named proof_of_address would come back as proof_of_address_url.

The message part

A JSON string (not a nested object):

{
"rpcQueue": "idv-gateway-v2",
"menu": "nafsi-ai",
"method": "init",
"no_adapter": false,

"client_id": "88447c1a-4aa7-11f1-b36a-ba8f5d272487",
"work_flow_id": "660db29a-58f0-11f1-a6ba-8a4d7ac10c49",
"config": "kenya_national_id",
"country": "Kenyan",

"request_id": "3e7967dd-60dc-43e5-9523-9663bef9c3d1"
}
FieldRequiredMeaning
rpcQueueyesAlways idv-gateway-v2. A value the platform does not know is rejected with code 101
menuyesAlways nafsi-ai for verifications
methodyesAlways init
no_adapteryesMust be false. true stores the files and stops — it does not run a verification
client_idyesYour API client
work_flow_idyesWhich workflow to run — decides which checks execute and in what order
configyesThe document type. Determines the zones, the fields returned, and the validation rules. See Document types
countrynoFree-text label carried through to the result
request_idnoYour idempotency/correlation key. Generated if omitted, and always echoed back

Anything else you add is carried through to the response untouched, so you can attach your own reference IDs.

Example

curl -X POST https://apisv2.windeal.co.ke/upload \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-F "front=@front.jpg" \
-F "back=@back.jpg" \
-F "selfie=@selfie.jpg" \
-F 'message={
"rpcQueue":"idv-gateway-v2",
"menu":"nafsi-ai",
"method":"init",
"no_adapter":false,
"client_id":"'"$CLIENT_ID"'",
"work_flow_id":"'"$WORKFLOW_ID"'",
"config":"kenya_national_id",
"country":"Kenyan"
}'
Send the capture as taken

Do not crop, deskew or downscale to "help". The platform does card detection, rectification and cropping itself, and a pre-cropped card removes the border evidence the document-authenticity checks rely on. Cap the long edge around 1600px and use JPEG quality ~0.7; beyond that you are paying upload time for detail the models do not use.

Timeouts

A verification runs its checks server-side and the call stays open until they finish. Allow at least 120 seconds. Typical completion is 15–30s; the first verification after a platform restart is slower because the models load on demand.

Retry only on a transport failure, and only once. An HTTP error status is a result, not a transport failure — resubmitting it runs the verification again and bills again.


3. The response

200 OK with the completed verification as JSON. Real response, abridged only by trimming the sanctions list bodies:

{
"idv_unique_id": "7bf9ba15b9304dfebe095be361d1da87",
"client_id": "88447c1a-4aa7-11f1-b36a-ba8f5d272487",
"work_flow_id": "660db29a-58f0-11f1-a6ba-8a4d7ac10c49",
"status": "success",
"verification_decision": "accept",
"decision_reasons": [],
"flags": [],

"config": "kenya_national_id",
"document_type": "kenya_national_id",
"country": "Kenyan",

"field_schema": [
"national_id_number", "serial_number", "full_name", "date_of_birth",
"sex", "place_of_birth", "date_of_issue", "district", "division",
"location", "sub_location", "document_type"
],
"combined": {
"national_id_number": "24150066",
"serial_number": "237769356",
"full_name": "JUMA GEORGE ODHIAMBO SURE",
"date_of_birth": "02.04.1984",
"sex": "M",
"place_of_birth": "MURANGA EAST",
"date_of_issue": "13.01.2014",
"district": "RACHUONYO NORTH",
"division": "KARACHUONYO E",
"location": "WANG ' CHIENG",
"sub_location": "KAMSERSEKA",
"document_type": "national_id"
},

"photo_comparison": {
"decision": "match",
"match": true,
"distance": 0.3595,
"similarity": 0.6405,
"threshold": 0.42,
"review_band": 0.13,
"id_face_found": true,
"selfie_face_found": true,
"model": "InsightFace ArcFace (buffalo_l)"
},
"liveness_detection": {
"status": "success",
"liveness": true,
"confidence": 0.961,
"threshold": 0.35,
"method": "minifasnet"
},
"document_authenticity": {
"status": "success",
"decision": "PASS",
"overall_authentic": true,
"reason_codes": [],
"notes": ["AUTO_APPROVED_SECURITY_FEATURES"],
"checks_applied": ["substrate", "doc_type", "cross_check", "mrz", "guilloche"],
"checks_skipped": ["seal", "hologram", "barcode"],
"scores": { "medium": "PLASTIC", "paper_score": 0.498, "guilloche_score": 6.598,
"mrz_all_checks_pass": true, "ocr_score": 71 },
"ocr_quality": {
"score": 71, "coverage": 1.0, "agreement": 1.0, "mrz_trusted": true,
"conflicts": [], "unread": []
}
},
"sanctions_screening": { "status": "success", "total_hits": 0, "ofac": {}, "pep": {} },

"mrz_parsed": {
"document_number": "237769356",
"date_of_birth": "1984-04-02",
"sex": "M",
"all_checks_pass": true
},

"images": {
"front_id": "https://files.nafsi.ai/uploads/.../front.jpg",
"back_id": "https://files.nafsi.ai/uploads/.../back.jpg",
"selfie": "https://files.nafsi.ai/uploads/.../selfie.jpg",
"front_rectified": "https://files.nafsi.ai/uploads/.../front_rectified.jpg",
"back_rectified": "https://files.nafsi.ai/uploads/.../back_rectified.jpg",
"id_photo": "https://files.nafsi.ai/uploads/.../id_photo.jpg",
"id_face": "https://files.nafsi.ai/uploads/.../id_face.jpg",
"selfie_face": "https://files.nafsi.ai/uploads/.../selfie_face.jpg"
},

"performance_metrics": { "total_duration_ms": 15179, "step_count": 6, "step_timings": [] }
}

The two fields to branch on

verification_decisionaccept | review | reject. This is the platform's overall verdict and the one to automate against.

decision_reasons — why, when it is not accept. Values seen in production include face_no_match, doc_not_authentic, doc_confidence_below_review_band, face_low_confidence.

Individual blocks (document_authenticity.decision, photo_comparison.decision) are the per-check verdicts that fed it. Read them for detail; do not reimplement the aggregation.

combined and field_schema

field_schema lists the fields this document type carries, in the order they appear on the card, and combined contains exactly those keys — nothing else, and every one of them present.

This matters when you render or store results:

  • A field that is absent from field_schema does not exist on that document. A Kenyan ID has no expiry date; a Ugandan ID has no serial number. Do not show an empty row for it, and do not treat its absence as a failure.
  • A field in field_schema but empty in combined is a field the card has that we could not read. That is a gap worth surfacing to a reviewer, and it is deliberately distinct from the case above.

document_authenticity.ocr_quality quantifies the same thing: coverage is how much of the card was read, unread names what was missed, and conflicts names any field where the printed value disagrees with the MRZ.

images

Every URL is on files.nafsi.ai and covers both the originals you sent and the derived crops the checks produced — the rectified card faces, the ID portrait, and the two face crops the match distance was computed from. Useful for a review queue: a human can see exactly what the model saw.


Document types

config selects the document definition. Currently configured:

configDocument
uganda_national_id_newUganda National ID (new generation)
uganda_national_id_oldUganda National ID (old generation)
kenya_national_idKenya National ID
ke_national_idKenya National ID (alternate layout)
ke_maisha_cardKenya Maisha Card
ke_passportKenya Passport
uganda_driving_licenceUganda Driving Licence

The set is configuration, not code — ask Nafsi rather than assuming a document is unsupported. Each definition carries its own field zones and validation rules, which is why field_schema differs per type.


Errors

Failures come back with a numeric code in v_field39 and a message in v_field48.

CodeMeaningWhat to do
101Unknown rpcQueueSend idv-gateway-v2
102Missing rpcQueue, menu or methodFix the message part
401Missing or malformed AuthorizationSend Bearer <access_token>
401Invalid or expired tokenRefresh, then retry once
403Client ID mismatchThe client_id in message is not the token's owner
230 / 502Upstream verification failedTransient; safe to retry once

An HTTP-level error is a result. Only a dropped connection or a timeout is worth an automatic retry, and only once — the verification may already have run.


Webhooks

/upload returns the finished verification, so webhooks are not required for this flow. They are worth adding for verifications that end in review and are later resolved by an operator, since that outcome arrives long after your request returned. See Webhook Management.