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:
POST /postdatawithmenu: auth_config— exchange your client credentials for an access token.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
| Part | Type | Required | Notes |
|---|---|---|---|
front | file | yes | Front of the document |
back | file | yes | Back of the document. For a passport, send the same image as front |
selfie | file | no | Required for face matching and liveness; omit for a document-only check |
message | text | yes | JSON 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"
}
| Field | Required | Meaning |
|---|---|---|
rpcQueue | yes | Always idv-gateway-v2. A value the platform does not know is rejected with code 101 |
menu | yes | Always nafsi-ai for verifications |
method | yes | Always init |
no_adapter | yes | Must be false. true stores the files and stops — it does not run a verification |
client_id | yes | Your API client |
work_flow_id | yes | Which workflow to run — decides which checks execute and in what order |
config | yes | The document type. Determines the zones, the fields returned, and the validation rules. See Document types |
country | no | Free-text label carried through to the result |
request_id | no | Your 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_decision — accept | 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_schemadoes 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_schemabut empty incombinedis 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:
config | Document |
|---|---|
uganda_national_id_new | Uganda National ID (new generation) |
uganda_national_id_old | Uganda National ID (old generation) |
kenya_national_id | Kenya National ID |
ke_national_id | Kenya National ID (alternate layout) |
ke_maisha_card | Kenya Maisha Card |
ke_passport | Kenya Passport |
uganda_driving_licence | Uganda 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.
| Code | Meaning | What to do |
|---|---|---|
101 | Unknown rpcQueue | Send idv-gateway-v2 |
102 | Missing rpcQueue, menu or method | Fix the message part |
401 | Missing or malformed Authorization | Send Bearer <access_token> |
401 | Invalid or expired token | Refresh, then retry once |
403 | Client ID mismatch | The client_id in message is not the token's owner |
230 / 502 | Upstream verification failed | Transient; 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.