From a raw ID photo to verified data — architecture, code, and the pitfalls that quietly break onboarding.
Identity verification is the front door of any fintech, marketplace, or rental product. A user photographs their ID card, and within seconds your system must know: who is this, did we read it confidently, and is it safe to proceed? This article walks that flow down to the code.
The shape of the flow
A healthy e-KYC flow has four stages, and three of them happen on your backend — not on the user's phone:
- •The client uploads the ID photo to your backend (not directly to a third-party API).
- •Your backend calls OCR to turn the image into structured fields.
- •Your backend validates the result: NIK format, field completeness, confidence level.
- •Data is stored; ambiguous cases go to a manual review queue.
Calling KTP OCR
The endpoint takes an image as multipart or base64 and returns structured JSON along with a confidence level:
curl -X POST "https://v1.api.my.id/ocr/ktp" \ -H "Authorization: Bearer $API_KEY" \ -F "image=@ktp.jpg"
The response carries every field printed on the card, plus a confidence value that should drive what you do next:
{
"status": "success",
"points_charged": 150,
"data": {
"nik": "3204xxxxxxxxxxxx",
"nama": "BUDI SANTOSO",
"tempat_lahir": "BANDUNG",
"tanggal_lahir": "17-08-1990",
"jenis_kelamin": "LAKI-LAKI",
"alamat": "JL. MERDEKA NO. 1",
"kecamatan": "COBLONG",
"confidence": "high"
}
}Never trust raw OCR output
This is the step teams skip most often. OCR turns pixels into text; it does not guarantee that text makes sense. A blurry photo, a glare spot, or a bent card can yield a NIK with two digits transposed. Validation is not optional.
The NIK has structure you can check without calling any service: 16 digits, the first six encode the region (province, regency, district), the next six encode the birth date — with 40 added to the day for women.
function nikIsPlausible(nik, birthDate, gender) {
if (!/^\d{16}$/.test(nik)) return false;
let day = parseInt(nik.slice(6, 8), 10);
const isFemale = day > 40;
if (isFemale) day -= 40;
const month = nik.slice(8, 10);
const [d, m] = birthDate.split('-'); // "17-08-1990"
// the birth date encoded in the NIK must match the printed one
if (day !== parseInt(d, 10) || month !== m) return false;
// gender must be consistent with the +40 rule
return isFemale === (gender === 'PEREMPUAN');
}This cross-check catches the most dangerous class of OCR error: a single transposed digit in the NIK will almost always break the birth-date match.
Cross-check the address against region data
The next step raises data quality sharply: confirm the district you read actually exists. The Regions API validates it and hands you the official code to store — far more useful than a free-text string.
curl "https://v1.api.my.id/wilayah/search?q=coblong" \
-H "Authorization: Bearer $API_KEY"
# → { "code": "3273090", "name": "COBLONG", "level": "district" }Store 3273090, not "Coblong". The code is stable, joinable, and saves you from three spellings of the same place.
The decision rule
Combine the signals above into one clear decision — and above all, keep a human path open for the grey zone:
- •Auto-approve —
highconfidence, consistent NIK, valid district. - •Manual review —
mediumconfidence, or any single check fails. - •Ask for a retake —
lowconfidence, or required fields are null.
Cost and failure
One KTP scan costs 150 points (equivalent to Rp 150), and only on success. If the upstream call fails, points are refunded automatically — you never pay for an error. An empty balance returns 402 stating how much was needed versus your balance, which makes it easy to trigger an auto top-up.
Finally: treat ID photos as the most sensitive data in your system. Do not keep the raw image longer than the verification itself requires.
Ready to build?
Create a free account and get your API key — 5,000 free hits a month, and two endpoints that need no key at all.