KYC & Identity Verification¶
How it works¶
KYCVerificationWidget (django_camera_kit.kyc.widgets.KYCVerificationWidget) is a forms.HiddenInput subclass with no file-upload fallback — unlike the scan widget, allowing a raw upload here would defeat the whole point of the liveness challenge.
- ID capture — the rear camera opens with a guide matching the real ISO/IEC 7810 ID-1 card ratio (85.6 × 53.98mm), a single shot, no auto-crop.
- Selfie + liveness challenge — the front camera opens with a face-oval guide. Every frame runs through
cv.FaceDetectorYN(YuNet). Once a face is reliably detected, the user is asked to turn their head slightly; the challenge completes when the detected face's horizontal center has moved far enough, at which point the current frame is captured automatically. - Submit — both images plus a
liveness_passedflag are POSTed to yourverify_url(afetch()call with the CSRF token read from thecsrftokencookie). - Server-side matching —
KYCVerifyViewextracts a face embedding from each image (insightface,buffalo_scmodel) and computes cosine similarity. Matching only ever happens server-side — the client never computes or asserts a match score. - Result — the response (
status,match_score,liveness_passed,created_at) is used to populate the hidden field with the verification id on success.
Why head movement, not blink¶
YuNet returns 5 point landmarks (eye/nose/mouth centers) — not full eyelid contours, so there's no way to compute a real eye-aspect-ratio blink signal from it. Tracking horizontal head movement is a substitute that doesn't depend on eye-region precision (or lighting/skin-tone-sensitive texture analysis) at all.
Security model — what's real, what isn't yet¶
- Identity binding: every
KYCVerificationrequires an authenticated user (permission_classes = [IsAuthenticated]) and is bound torequest.user. This is enforced at the view regardless of your project's global DRF settings. - Rate limiting:
KYCVerifyThrottlelimits requests per user (THROTTLE_RATE, default10/hour) — each request runs a full face-embedding inference pass, so an unthrottled endpoint is a cheap resource-exhaustion vector. - Upload size limits:
MAX_UPLOAD_SIZE_MB(default 8) rejects oversized uploads before they're processed. liveness_passedis client-asserted, not server-verified. A malicious client could sendliveness_passed=truewithout ever performing the head-movement challenge. This is a known, documented MVP limitation — real server-side liveness verification (passive scoring from the images themselves, multiple challenge types) is planned but not yet built. Don't treat the current liveness flag as a strong anti-spoofing guarantee.- Storage: the API response never includes URLs to the stored
selfie/id_documentimages. Whether they're otherwise exposed depends entirely on your storage backend configuration — see Installation.
Vector search beyond 1:1 matching¶
selfie_embedding and id_face_embedding are stored as pgvector.django.VectorField with an HNSW index (cosine distance), not just used transiently for the 1:1 comparison. This means a similarity search across all stored embeddings — e.g. KYCVerification.objects.order_by(CosineDistance("selfie_embedding", target)) — can surface the same face reused across multiple accounts, which is the basis for network-level fraud detection beyond what a single verification's match score can tell you. Django Camera Kit stores the data to make this possible; building the actual fraud-detection query/workflow on top is left to your project for now.
Settings¶
See Settings reference for EMBEDDING_MODEL, MATCH_THRESHOLD, THROTTLE_RATE, and MAX_UPLOAD_SIZE_MB.