Skip to main content

FKTD — Fraud KYC KTP Detector

PyPI version Python versions License: MIT

Multi-engine OCR plus strict cross-validation for Indonesian ID cards (KTP).

Most validators only check whether a field is empty. FKTD also decodes the 16-digit NIK, which encodes the cardholder's birth date, sex, province, and regency, and checks those against the printed fields. A card where the NIK says South Jakarta but the printed city says West Jakarta will fail, even though every field is filled in.

This is structural validation, not identity verification. It does not talk to Dukcapil. See Disclaimer.

Contents

Pipeline

image -> OCR (EasyOCR / PaddleOCR) -> field extraction -> fraud rules -> FraudResult

process() runs OCR, parses the text into 19 fields, detects the photo and signature, runs the rule engine, and returns a FraudResult with is_passed, a risk_score (0–100), and the per-rule breakdown.

Requirements

Minimum Notes
Python 3.9 CPython, 64-bit. Tested through 3.12.
OS macOS / Linux / Windows Apple Silicon works without extra setup.
RAM ~4 GB EasyOCR loads PyTorch and model weights into memory.
Disk ~2–3 GB PyTorch and OpenCV wheels are large; EasyOCR weights add ~100 MB.
Network once EasyOCR downloads weights on first run. The region API is optional.

Core dependencies (pulled in automatically): easyocr>=1.7 (brings PyTorch), opencv-python>=4.10, Pillow>=10.4, numpy>=1.26, pydantic>=2.8, python-dateutil>=2.9.

The first FKTD() call downloads EasyOCR weights and is slow. After that it runs offline unless you enable the live region API.

Install

pip install fktd

EasyOCR is included and needs no system binaries. PaddleOCR is optional:

pip install "fktd[paddle]"   # paddlepaddle is unreliable on Apple Silicon

From source:

git clone https://github.com/rezayw/fktd.git
cd fktd
pip install -e ".[dev]"      # pytest + ruff
Engine Status Notes
EasyOCR included Default. Indonesian out of the box, fine on macOS.
PaddleOCR extra [paddle] Higher accuracy/speed; best on Linux.

Usage

from fktd import FKTD

detector = FKTD(engine="easyocr")        # or engine="paddle"
result = detector.process("ktp.jpg")

result.is_passed          # bool
result.risk_score         # 0 (safe) .. 100 (suspicious)
result.summary()          # failed checks as text
result.data.model_dump()  # all extracted fields

summary() looks like:

[FAIL] risk_score=55
  - (critical) nik.province_match: Provinsi di NIK (DKI JAKARTA) != field (JAWA BARAT)
  - (critical) vision.foto: Foto tidak terdeteksi

API

FKTD(...)

FKTD(
    engine="easyocr",          # "easyocr" | "paddle" | an OCREngine instance
    lang=None,                 # OCR language override
    risk_threshold=25,         # pass requires risk_score < this
    min_confidence=60.0,       # OCR confidence floor for critical fields
    detect_vision=True,        # run photo + signature detection
    wilayah_provider="auto",   # "auto" | "api" | "local"
    **engine_kwargs,           # forwarded to the engine
)
Method Returns Notes
process(image, today=None) FraudResult Full pipeline.
extract_fields(image) KTPData OCR + extraction only, no rules.

image accepts a path, bytes, a PIL image, or a NumPy array. today overrides the reference date used for age and birth-date checks (useful in tests).

FraudResult

Field Type Notes
is_passed bool Overall result.
risk_score int 0–100.
checks list[FraudCheck] Every rule, passed and failed.
failed_checks list[FraudCheck] Failed rules only (property).
data KTPData Extracted fields.
summary() str Status plus failed checks.

FraudCheck carries code, passed, severity (info / warning / critical), message, and weight (added to risk_score on failure).

Extracted fields

19 text fields: Province, City/Regency, NIK, Name, Place of Birth, Date of Birth, Sex, Blood Type, Address, RT/RW, Village (Kel/Desa), District (Kecamatan), Religion, Marital Status, Occupation, Citizenship, Valid Until, Place Issued, Date Issued.

Two boolean flags from vision: has_foto, has_tanda_tangan.

Fraud rules

Each rule emits a FraudCheck. A failure adds its weight to risk_score, capped at 100.

Completeness — 16 required fields. Empty is CRITICAL, weight 15 each. Blood type and the issued-at/on fields are not required.

NIK cross-check (values encoded in the NIK):

Check Severity Weight
NIK is exactly 16 digits CRITICAL 30
Birth date in NIK == Date of Birth field CRITICAL 30
Sex in NIK (day > 40 means female) == Sex field CRITICAL 25

Location (NIK codes vs. official region data):

Check Severity Weight
Province code (2 digits) known and matches Province CRITICAL 25
Regency code (4 digits) known and matches City/Regency CRITICAL 25

Enums (exact match):

Field Allowed Severity Weight
Sex LAKI-LAKI, PEREMPUAN CRITICAL 20
Religion ISLAM, KRISTEN, KRISTEN PROTESTAN, KATOLIK, HINDU, BUDHA, KONGHUCU CRITICAL 20
Citizenship WNI, WNA CRITICAL 20
Marital Status BELUM KAWIN, KAWIN, CERAI HIDUP, CERAI MATI WARNING 10
Blood Type A, B, AB, O, - WARNING 5

Text format:

  • Field values must be uppercase (WARNING, 10).
  • Name must not contain digits (CRITICAL, 20).
  • RT/RW matches NNN/NNN. Birth date must parse, not be in the future (CRITICAL, 25), and imply an age <= 120.
  • "Valid Until" must be SEUMUR HIDUP (CRITICAL, 25). e-KTPs since 2016 are valid for life; an explicit expiry date means an old or invalid card.

OCR confidence — confidence below min_confidence on NIK, Name, or Date of Birth adds WARNING (8).

Vision — missing photo is CRITICAL (20); missing signature is WARNING (10).

Pass criteria

is_passed is true only when all of:

  1. all required fields present,
  2. no failed CRITICAL checks,
  3. risk_score < risk_threshold (default 25).

Region data

wilayah_provider controls where region codes are resolved:

Mode Behaviour
auto (default) Local snapshot first, live API for codes the snapshot lacks.
api Always hit the live region API (emsifa).
local Snapshot only, no network.
FKTD(wilayah_provider="local")   # no outbound requests

Disclaimer

e-KTP only. FKTD targets the electronic KTP (KTP-el), the standard since 2011, valid for life. Rules like "Valid Until must be SEUMUR HIDUP" do not apply to pre-2011 cards that carry an expiry date; those will fail because they are no longer valid.

Structural, not official. NIK and region checks validate the structure and internal consistency of the data on the card. FKTD does not access Dukcapil, so it cannot confirm a NIK is registered or belongs to a real person. PASS does not prove the card is genuine; FAIL does not prove forgery (it can be a bad scan, an old card, or an OCR miss). risk_score is an indicator, not a decision.

For production KYC, combine FKTD with official verification (Dukcapil or a licensed NIK provider) and manual review where needed.

Privacy. A KTP holds personal data. Make sure your use complies with Indonesia's UU PDP (Law No. 27/2022): lawful basis or consent, limited retention, secured storage. FKTD never uploads card images; the only outbound traffic is the optional region API (province/regency codes), which wilayah_provider="local" disables.

License

MIT

Release files for fktd 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fktd 0.1.1
File Size Uploaded
fktd-0.1.1.tar.gz 32.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fktd 0.1.1
File Interpreter ABI Platform
fktd-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 59.2 kB

Release files / fktd-0.1.1.tar.gz

Download URL fktd-0.1.1.tar.gz
Size 32.6 kB
Tags Source
SHA-256 checksum
How to use checksums
66d695eca33db79af3dd01faf7983606765cc6815693658dce4e870b208c853a
BLAKE2b-256 checksum
How to use checksums
4899d590d3c1e06c4ecd1d65b9ca0ea33173f62b813f87dd51809bfbc61eafff
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.4

Release files / fktd-0.1.1-py3-none-any.whl

Download URL fktd-0.1.1-py3-none-any.whl
Size 26.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2141dac624214d6fba8d1166211b7fa0ed4ec66aa2d0d02810ef5f86ae71921a
BLAKE2b-256 checksum
How to use checksums
a15e2f4d3031ff17de7017610883de7d20f96637781e9ae18e998e170b76b03f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.4

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page