Face Recognition

POST/v1/face/recognitionsEnhanced due diligence

Match a live face against the enrolled faces of the RDCPASS national digital identity. Use it to confirm that the person in front of your camera is who they claim to be, or to establish who an undocumented person is. On a match, RDCPASS returns the same identity data as KYC Validation, filtered by the scopes your application holds. Every call requires the authentication stack described in Authentication.

At a glance

  • Endpoints: POST /v1/face/recognitions (single) and POST /v1/face/recognitions/batches (batch).
  • Two modes: 1:N identification (image only) and 1:1 verification (image + identifier).
  • Passive liveness detection runs by default — no head movements or blinking required from the user.
  • Default similarity threshold of 0.85, adjustable per request.
  • A human-readable reason is mandatory on every call and is stored in RDCPASS’s audit trail.
  • Result match returns account, kyc.basic and your granted scopes; no_match returns no identity data.
  • Single, batch, synchronous and asynchronous modes; async results arrive on the face_recognition.completed webhook.
  • Production access requires enhanced due diligence — see Production requirements below.

Identification (1:N) vs verification (1:1)

The same endpoint serves two distinct questions. Which one you ask is decided solely by whether you send an identifier; the response reports it in mode.

ModeHow to callQuestion answeredTypical uses
verification (1:1)Send image and identifier.Is this face the enrolled face of RDCPASS identity COD-2103-0214-4937?Account opening, step-up authentication, SIM swap approval, cash-out at an agent, remote onboarding with a claimed ID.
identification (1:N)Send image only.Which certified RDCPASS identity, if any, does this face belong to?Identifying an unconscious patient, a beneficiary who lost their documents, or a suspected duplicate registration.

Prefer verification whenever you know the claimed identity

A 1:1 comparison is more accurate, faster and far less intrusive than a search across the national population. Identification is reserved for situations where no claimed identity is available, and its reason policy is reviewed more strictly during due diligence.

Image requirements

Send the image either inline as base64 (image.data) or as an HTTPS URL RDCPASS can fetch (image.url). Images that fail these requirements are rejected with a 422 error and are not billed.

  • Format: JPEG or PNG.
  • Resolution: at least 480 × 640 pixels, portrait orientation recommended.
  • Exactly one face, looking at the camera, fully visible from forehead to chin — no sunglasses, masks or hands over the face.
  • Even, frontal lighting; avoid strong backlight and heavy compression.
  • A photo taken live by your app or kiosk. Scans of identity documents and photos of screens fail liveness.
  • image.url must be publicly reachable over HTTPS for the duration of the request (a short-lived pre-signed URL is ideal).

Passive liveness

With liveness set to passive (the default), RDCPASS analyses the same single image for signs of a presentation attack — printed photos, screen replays, masks — before comparing faces. The user does nothing special. The outcome is reported in the liveness object (checked, passed, score). If the check fails, the call returns 422 liveness_failed and no comparison is performed.

Set liveness to none only when the image comes from a controlled capture where liveness is already guaranteed — for example an attended branch camera. The response then reports "checked": false with passed and score set to null. Your due-diligence dossier must justify every application that disables liveness.

Similarity threshold

similarity is a score from 0 to 1 expressing how closely the submitted face matches the enrolled face. The result is match when similarity is greater than or equal to threshold. The default of 0.85 balances false accepts and false rejects for most onboarding flows; tune it to your risk.

thresholdUse when
0.80Low-risk, high-volume checks where a false reject costs more than a false accept (e.g. event ticketing).
0.85 (default)Standard onboarding and step-up authentication.
0.90 – 0.95High-value transactions, SIM swap approval, and every 1:N identification.

The mandatory reason field

Biometric matching against the national identity is a significant intrusion into a citizen’s privacy, so every call must say why it is being made. reason is a free-text, human-readable justification written for an auditor, not for a machine. RDCPASS stores it with the request, the calling application, the member or system that triggered it and the result, and the audit trail is reviewed by RDCPASS and made available to the data-protection authority.

A good reason is specific and traceable

  • "Current account opening at Gombe branch, Kinshasa - applicant present at counter 3, ticket KIN-GOM-2026-004812"
  • "SIM swap request for +243812345678 at Vodacom shop, Lubumbashi centre - agent ID LUB-117"

Reasons that will be flagged in review

  • "test"
  • "KYC"
  • "verification"
  • The same static string on every request.
Your production application is approved against a documented reason policy (see Production requirements). Reasons that do not fit that policy, or that are generic, are flagged in RDCPASS’s audits and can lead to suspension of the service.

Request body

Plaintext JSON shape, before encryption. Common request fields are described once in KYC scopes & subscriptions.

FieldTypeRequiredDescription
imageobjectYesThe face to match: { "data": "<base64>" } or { "url": "https://…" }. JPEG/PNG, at least 480 × 640, one face.
identifierobjectFor 1:1The claimed identity, { "type", "value" } with type one of rdcpass_id, passport, ceni, driving_licence, national_id. Present ⇒ 1:1 verification; absent ⇒ 1:N identification.
livenessstring enumNopassive (default) or none. See Passive liveness.
thresholdnumberNoMinimum similarity for a match, from 0 to 1. Default 0.85.
reasonstringYesHuman-readable justification for this specific check. Stored and audited. Maximum 500 characters.
purposestring enumYesOne of the purposes approved for your application, e.g. customer_onboarding, transaction_authorization, fraud_prevention.
scopesstring[]NoSubset of your application’s granted scopes to return on a match. Defaults to all granted scopes.
document_deliverystring enumNosigned_url (default) or base64 for document images and biometric files. See Documents & biometrics.
referencestringNoYour own identifier for this check, echoed back in the response and webhooks.
Request body — 1:1 verification (plaintext, before encryption)
{
  "image": { "data": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8U..." },
  "identifier": { "type": "rdcpass_id", "value": "COD-2103-0214-4937" },
  "liveness": "passive",
  "threshold": 0.85,
  "reason": "Current account opening at Gombe branch, Kinshasa - applicant present at counter 3, ticket KIN-GOM-2026-004812",
  "purpose": "customer_onboarding",
  "scopes": ["kyc.documents.primary", "kyc.addresses", "kyc.phone_numbers", "kyc.biometrics.selfie"],
  "document_delivery": "signed_url",
  "reference": "cust-0001"
}
Request body — 1:N identification (plaintext, before encryption)
{
  "image": { "url": "https://uploads.example.cd/kyc/selfies/7f3a1c9e.jpg" },
  "liveness": "passive",
  "threshold": 0.90,
  "reason": "Unconscious patient admitted to Hôpital du Cinquantenaire emergency ward without documents - identification requested by duty physician Dr. Mbuyi Ilunga Nsimba",
  "purpose": "regulatory_compliance",
  "reference": "adm-2026-09-26-0147"
}

Response body

A successful call returns 200 OK. A no_match is a successful call, not an error.

FieldTypeDescription
idstringUnique identifier of the check, prefixed face_.
objectstringAlways face_recognition.
livemodebooleantrue in production, false in the sandbox.
referencestring | nullThe reference you sent, or null.
statusstring enumAlways completed for synchronous calls. Async and batch items can be failed.
modestring enumverification (1:1) or identification (1:N).
resultstring enummatch or no_match.
similaritynumber | nullSimilarity score from 0 to 1. In 1:N identification, null on no_match so that no candidate is disclosed.
thresholdnumberThe threshold applied to this check.
livenessobject{ "checked", "passed", "score" } — the passive liveness outcome.
identifierobjectThe identifier you sent (1:1 only).
accountobjectMatch only. RDCPASS account: rdcpass_id, status, certified, created_at, level_of_assurance.
kycobjectMatch only. basic is always present; every other block depends on the scopes applied.
scopes_appliedstring[]Scopes whose data is included, always starting with kyc.basic on a match. Empty on no_match.
scopes_withheldstring[]Requested and granted scopes with no data on record for this person — omitted, not an error.
purposestringThe purpose you declared.
reasonstringThe reason you sent, exactly as recorded in the audit trail.
created_atstringWhen the check was performed (ISO 8601, UTC).
resultMeaningIdentity data returned
matchThe face matches a certified RDCPASS identity with similarity ≥ threshold (the claimed one in 1:1 mode).account, kyc.basic and every applied scope.
no_match1:1 — the face does not match the claimed identity, or that identity has no certified account. 1:N — no certified identity reaches the threshold.None.

On a match, the KYC scope model applies unchanged

The kyc block is built exactly as for KYC Validation: basic KYC is always returned, and additional blocks (documents, addresses, phone numbers, biometrics…) only for scopes your organization subscribes to, your application has selected, and your request did not narrow away. Read KYC scopes & subscriptions for the full model.
200 OK — match (after decryption, every scope granted)
{
  "id": "face_3c9a7e1f52",
  "object": "face_recognition",
  "livemode": true,
  "reference": "cust-0001",
  "status": "completed",
  "mode": "verification",
  "result": "match",
  "similarity": 0.96,
  "threshold": 0.85,
  "liveness": { "checked": true, "passed": true, "score": 0.97 },
  "identifier": { "type": "rdcpass_id", "value": "COD-2103-0214-4937" },
  "account": {
    "rdcpass_id": "COD-2103-0214-4937",
    "status": "active",
    "certified": true,
    "created_at": "2025-03-14T09:22:41Z",
    "level_of_assurance": "LOA3"
  },
  "kyc": {
    "basic": {
      "full_name": "Kabeya Mwamba Tshisekedi",
      "first_name": "Kabeya",
      "last_name": "Tshisekedi",
      "date_of_birth": "1988-04-12",
      "age": 38,
      "gender": "male",
      "nationality": "COD"
    },
    "documents": {
      "primary": {
        "type": "passport",
        "number": "OB1234567",
        "issued_at": "2022-06-02",
        "expires_at": "2032-06-01",
        "issuing_authority": "Direction Générale de Migration",
        "status": "valid",
        "image": {
          "content_type": "image/jpeg",
          "url": "https://files.rdcpass.cd/d/9f2c41e7b0?sig=Kx82hQ",
          "expires_at": "2026-09-26T10:20:02Z"
        }
      },
      "others": [
        {
          "type": "ceni",
          "number": "1234567890123",
          "issued_at": "2023-02-17",
          "expires_at": null,
          "issuing_authority": "Commission Électorale Nationale Indépendante",
          "status": "valid",
          "image": {
            "content_type": "image/jpeg",
            "url": "https://files.rdcpass.cd/d/4b7d02a9c1?sig=Pm31tZ",
            "expires_at": "2026-09-26T10:20:02Z"
          }
        }
      ]
    },
    "addresses": [
      {
        "type": "residential",
        "province": "Kinshasa",
        "city": "Kinshasa",
        "commune": "Gombe",
        "quartier": "Golf",
        "avenue": "Avenue de la Justice",
        "number": "45",
        "is_primary": true,
        "verified_at": "2025-03-14T09:40:12Z"
      }
    ],
    "emails": [
      { "address": "kabeya.tshisekedi@example.cd", "is_primary": true, "verified": true }
    ],
    "phone_numbers": [
      { "number": "+243812345678", "operator": "Vodacom", "is_primary": true, "verified": true },
      { "number": "+243970112233", "operator": "Airtel", "is_primary": false, "verified": true }
    ],
    "professions": [
      { "title": "Ingénieur réseaux", "employer": "Société Nationale d'Électricité", "sector": "energy", "since": "2016-09-01" }
    ],
    "place_of_birth": { "country": "COD", "province": "Kasaï-Oriental", "city": "Mbuji-Mayi" },
    "marital_status": "married",
    "languages": ["fra", "lin", "lua"],
    "religion": "catholic",
    "ethnicity": "Luba",
    "biometrics": {
      "selfie": {
        "captured_at": "2025-03-14T09:31:55Z",
        "file": {
          "content_type": "image/jpeg",
          "url": "https://files.rdcpass.cd/d/c81e5f3a20?sig=Wq07vB",
          "expires_at": "2026-09-26T10:20:02Z"
        }
      },
      "fingerprints": [
        {
          "format": "ISO_19794_2",
          "finger": "right_index",
          "file": {
            "content_type": "application/octet-stream",
            "url": "https://files.rdcpass.cd/d/e2a9d7b614?sig=Hn55cR",
            "expires_at": "2026-09-26T10:20:02Z"
          }
        }
      ],
      "iris": [
        {
          "format": "ISO_19794_6",
          "eye": "left",
          "file": {
            "content_type": "application/octet-stream",
            "url": "https://files.rdcpass.cd/d/07f4b3c8e9?sig=Lt62xD",
            "expires_at": "2026-09-26T10:20:02Z"
          }
        }
      ]
    }
  },
  "scopes_applied": [
    "kyc.basic",
    "kyc.documents.primary",
    "kyc.documents.all",
    "kyc.addresses",
    "kyc.emails",
    "kyc.phone_numbers",
    "kyc.professions",
    "kyc.place_of_birth",
    "kyc.marital_status",
    "kyc.languages",
    "kyc.religion",
    "kyc.ethnicity",
    "kyc.biometrics.selfie",
    "kyc.biometrics.fingerprint",
    "kyc.biometrics.iris"
  ],
  "scopes_withheld": [],
  "purpose": "customer_onboarding",
  "reason": "Current account opening at Gombe branch, Kinshasa - applicant present at counter 3, ticket KIN-GOM-2026-004812",
  "created_at": "2026-09-26T10:15:02Z"
}
200 OK — no_match (after decryption)
{
  "id": "face_8e2b5d0a74",
  "object": "face_recognition",
  "livemode": true,
  "reference": "cust-0002",
  "status": "completed",
  "mode": "verification",
  "result": "no_match",
  "similarity": 0.41,
  "threshold": 0.85,
  "liveness": { "checked": true, "passed": true, "score": 0.95 },
  "identifier": { "type": "rdcpass_id", "value": "COD-1907-5521-0386" },
  "scopes_applied": [],
  "scopes_withheld": [],
  "purpose": "customer_onboarding",
  "reason": "Mobile money wallet upgrade to tier 2 - selfie captured in app, session KIN-APP-88213",
  "created_at": "2026-09-26T10:16:40Z"
}

Example request

A 1:1 verification in eight languages. Header values are illustrative — see Authentication to compute a real signature, encrypt the body and set X-RDCPASS-IV.

verify-face.sh
# Plaintext body shown for readability - encrypt and sign it as described in /docs/authentication before sending.
IMAGE_B64=$(base64 < selfie.jpg | tr -d '\n')

curl https://api.rdcpass.cd/v1/face/recognitions \
  -X POST \
  -H "X-RDCPASS-Key-Id: key_live_8f2a1c0e9b" \
  -H "X-RDCPASS-Timestamp: 1735689600" \
  -H "X-RDCPASS-Nonce: 2d7e4b1a-9c3f-4a58-b6e2-7f0c1d9a3e55" \
  -H "X-RDCPASS-IV: q3Jx8mT2vLp9Wc1R" \
  -H "X-RDCPASS-Signature: 8b4e1f...c93a" \
  -H "Content-Type: application/json" \
  -d '{
    "image": { "data": "'"$IMAGE_B64"'" },
    "identifier": { "type": "rdcpass_id", "value": "COD-2103-0214-4937" },
    "liveness": "passive",
    "threshold": 0.85,
    "reason": "Current account opening at Gombe branch, Kinshasa - applicant present at counter 3, ticket KIN-GOM-2026-004812",
    "purpose": "customer_onboarding",
    "reference": "cust-0001"
  }'

Request modes

Face Recognition supports all four request modes. Add Prefer: respond-async to run asynchronously and Idempotency-Key to retry safely. Details in Single, batch & async.

Synchronous (default)Asynchronous (Prefer: respond-async)
Single — POST /v1/face/recognitions200 with the result.202 with a job; result via the face_recognition.completed webhook or GET /v1/jobs/{job_id}.
Batch — POST /v1/face/recognitions/batches200 with all results, up to 50 items.202 with a batch, up to 10,000 items; results via webhook or GET /v1/batches/{batch_id}/results.

Batch example

Each item carries its own image, optional identifier and required reference. Batch-level purpose, reason, liveness and scopes apply to every item unless an item overrides them — an item-level reason is recommended whenever the justification differs per person. Prefer image.url in batches to keep payloads small. A failed item (for example no face in the image) does not fail the batch.

Synchronous batch (up to 50 items)
curl https://api.rdcpass.cd/v1/face/recognitions/batches \
  -X POST \
  -H "X-RDCPASS-Key-Id: key_live_8f2a1c0e9b" \
  -H "X-RDCPASS-Timestamp: 1735689600" \
  -H "X-RDCPASS-Nonce: 2d7e4b1a-9c3f-4a58-b6e2-7f0c1d9a3e55" \
  -H "X-RDCPASS-IV: q3Jx8mT2vLp9Wc1R" \
  -H "X-RDCPASS-Signature: 8b4e1f...c93a" \
  -H "Idempotency-Key: 5b0e8c2a-71d4-4f93-a6b1-3e9d0c7f2a18" \
  -H "Content-Type: application/json" \
  --data-binary @batch.json   # Plaintext body shown for readability - encrypt and sign it as described in /docs/authentication before sending.
batch.json (plaintext, before encryption)
{
  "purpose": "fraud_prevention",
  "reason": "Quarterly re-verification of registered mobile money agents in Lubumbashi, compliance programme AGT-2026-Q3",
  "liveness": "passive",
  "items": [
    {
      "reference": "agent-LUB-0412",
      "image": { "url": "https://uploads.example.cd/agents/LUB-0412.jpg" },
      "identifier": { "type": "rdcpass_id", "value": "COD-2103-0214-4937" }
    },
    {
      "reference": "agent-LUB-0413",
      "image": { "url": "https://uploads.example.cd/agents/LUB-0413.jpg" },
      "identifier": { "type": "rdcpass_id", "value": "COD-1907-5521-0386" }
    },
    {
      "reference": "agent-LUB-0414",
      "image": { "url": "https://uploads.example.cd/agents/LUB-0414.jpg" },
      "identifier": { "type": "ceni", "value": "1234567890123" }
    }
  ]
}
200 OK (after decryption, abridged)
{
  "object": "list",
  "data": [
    {
      "reference": "agent-LUB-0412",
      "status": "succeeded",
      "result": {
        "id": "face_1a7c3e9b20",
        "object": "face_recognition",
        "mode": "verification",
        "result": "match",
        "similarity": 0.94,
        "liveness": { "checked": true, "passed": true, "score": 0.96 },
        "account": { "rdcpass_id": "COD-2103-0214-4937", "status": "active", "certified": true, "...": "..." },
        "kyc": { "basic": { "full_name": "Kabeya Mwamba Tshisekedi", "...": "..." } },
        "scopes_applied": ["kyc.basic"],
        "scopes_withheld": []
      }
    },
    {
      "reference": "agent-LUB-0413",
      "status": "succeeded",
      "result": {
        "id": "face_6d2f8a4c11",
        "object": "face_recognition",
        "mode": "verification",
        "result": "no_match",
        "similarity": 0.38,
        "liveness": { "checked": true, "passed": true, "score": 0.93 },
        "scopes_applied": [],
        "scopes_withheld": []
      }
    },
    {
      "reference": "agent-LUB-0414",
      "status": "failed",
      "error": {
        "code": "multiple_faces_detected",
        "message": "The image contains 2 faces. Submit an image with exactly one face."
      }
    }
  ],
  "next_cursor": null
}

Asynchronous example

Send the same body with Prefer: respond-async. RDCPASS answers immediately with 202 Accepted and a job; when the check completes, the face_recognition.completed event is delivered to your application’s webhook URL with the full result embedded. You can also poll GET /v1/jobs/{job_id}. Asynchronous batches return a batch object and emit batch.completed or batch.completed_with_errors. See Webhooks for signature verification.

202 Accepted
HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "id": "job_9e4c2a7b1d",
  "object": "job",
  "service": "face_recognition",
  "status": "pending",
  "created_at": "2026-09-26T10:15:00Z",
  "result_url": "/v1/jobs/job_9e4c2a7b1d"
}
Webhook event face_recognition.completed (abridged)
{
  "id": "evt_4d8b1e6f2a9c7035",
  "object": "event",
  "type": "face_recognition.completed",
  "livemode": true,
  "created_at": "2026-09-26T10:15:04Z",
  "data": {
    "object": {
      "id": "job_9e4c2a7b1d",
      "object": "job",
      "service": "face_recognition",
      "status": "completed",
      "created_at": "2026-09-26T10:15:00Z",
      "result_url": "/v1/jobs/job_9e4c2a7b1d",
      "result": {
        "id": "face_3c9a7e1f52",
        "object": "face_recognition",
        "reference": "cust-0001",
        "status": "completed",
        "mode": "verification",
        "result": "match",
        "similarity": 0.96,
        "liveness": { "checked": true, "passed": true, "score": 0.97 },
        "account": { "rdcpass_id": "COD-2103-0214-4937", "...": "..." },
        "kyc": { "basic": { "...": "..." } },
        "scopes_applied": ["kyc.basic", "kyc.documents.primary"],
        "scopes_withheld": []
      }
    }
  }
}

Production requirements

Face Recognition requires enhanced due diligence

Sandbox access is immediate, but production keys for an application with Face Recognition enabled are issued only after RDCPASS approves an enhanced due-diligence dossier. Typical review time is 10 to 20 business days.

In addition to the standard business and application due diligence, your compliance officer must provide:

  • A documented reason policy — the exhaustive list of situations in which your staff or systems trigger a face check, who may trigger it, the reason format they use, and whether 1:N identification is needed at all. Production reason values are audited against this policy.
  • A data-protection impact assessment (DPIA) covering the capture, transmission, retention and deletion of face images and of any biometric data returned.
  • Biometric-tier controls — the same rules as the kyc.biometrics.* scopes: legal basis, a named data-protection officer, a penetration-test report, encryption at rest, and a retention policy under which submitted images are deleted once the check is complete.
  • An on-site or video audit of the capture process (kiosk, branch or app), including how the citizen is informed before their face is captured.
  • Liveness justification for any flow that sets liveness to none.

The full process, statuses and document checklist are described in Going to production.

Errors

Errors use the standard format described in Errors. The codes below are the ones specific to, or most relevant for, Face Recognition. Rejected requests are not billed. A request without reason is rejected with 400 Bad Request.

CodeHTTPCauseWhat to do
no_face_detected422No face was found in the image.Recapture with the face centred and fully visible.
multiple_faces_detected422More than one face is visible.Recapture with only the subject in frame.
image_quality_insufficient422Resolution below 480 × 640, blur, overexposure or excessive compression.Recapture in better light; send the original, uncompressed image.
liveness_failed422The passive liveness check detected a presentation attack or could not confirm a live person.Ask the user to retake a live photo. Repeated failures for the same identity may indicate fraud.
invalid_identifier400The identifier value is malformed for its type.Check the format (e.g. COD-2103-0214-4937).
unsupported_identifier_type400The identifier type is not accepted.Use rdcpass_id, passport, ceni, driving_licence or national_id.
scope_not_granted403A requested scope is not granted to your application.Remove it from scopes, or have an administrator subscribe and select it.
purpose_not_approved403The purpose is not among those approved for your application.Use an approved purpose or request an update in the console.
service_not_enabled403Face Recognition is not enabled on this application.Enable the service on the application in the console.
production_access_required403Production keys used before enhanced due diligence was approved.Complete the process described in Going to production.
batch_too_large413A synchronous batch contains more than 50 items.Send it with Prefer: respond-async (up to 10,000 items).
insufficient_balance402Prepaid wallet is empty.Top up the wallet; see Billing.
rate_limited429Burst rate limit exceeded.Back off per retry_after and retry with the same Idempotency-Key.
422 Unprocessable Entity
{
  "error": "liveness_failed",
  "message": "Passive liveness check failed (score 0.22). The image appears to be a photo of a screen or a printed picture."
}

Next steps

Questions about your integration? Contact developer support