Accept a batch. Report each item.
Results arrive one at a time or in batches. Every item is validated on its own, so one incomplete record never hides the rest of the batch.
Identity verification platform at Lintasarta
One platform. Many banks.
Each sees only its own.

My work on digital identity began with models: detecting spoofed faces and reading identity documents.
In production, verification is more than a model. A result has to reach the right bank, be stored once, and stay readable for the people who review it. This case study follows that second part.
Anti-spoofing liveness detection and fine-tuned TrOCR for digital identity verification. One of Bangkit’s Best 5 capstone projects, selected from approximately 700 teams.
Liveness detection with YOLO and PyTorch. Model training using Jupyter on Kubeflow.
Multi-tenant e-KYC and asynchronous verification workflows, from partner integration to result retrieval.
The verification provider sends every result to a single callback, identified by one key for each environment. Each bank needs its results, and only its results.
I built the callback service from scratch between May and June 2025: it receives results, stores them in PostgreSQL, and routes each one to the bank it belongs to.
submissionId · userIdType · statusOne at a time or in a batchResults arrive one at a time or in batches. Every item is validated on its own, so one incomplete record never hides the rest of the batch.
A repeated submission ID is skipped and reported as a duplicate instead of failing the request, so a provider retry cannot create a second record.
Each result’s user ID type identifies the bank it belongs to. The record is stored for that bank, which retrieves results with its own key and sees only its own data.
Every request needs an API key, and keys are removed from each response and error message before it leaves the service.
From September 2025 to March 2026, I integrated the provider’s ID card (KTP) scan, liveness, and face match services, exposing each one to bank applications through the same sequence.
Partner headers and tokens are passed through and validated on every call, with clear errors when a required one is missing.
Request a partner token from the provider.
Open a verification session. A session ID is generated when the partner does not send one.
Upload through a presigned URL, then confirm the upload.
Poll the submission status and fetch the result details.
The dashboard is where bank staff and administrators follow verification results. I built the monitoring pages and the access model for bank operators.
Search, pagination, advanced and date filters, timestamps shown in WIB (UTC+7), and Excel export of liveness results.
Banks create operator accounts for their own staff. Officers sign in to their bank’s applications and liveness results, while agents and exports stay hidden from them.
A results page for ID card OCR with a table view and export, added to the dashboard navigation alongside face comparison.
Role-based access separates superadmins, admins, banks, and officers. Bank data is loaded before each officer route, account forms are validated and CSRF-protected, and password changes require verification.
A service is only as useful as the next person’s ability to run it. I documented what I built for the team and for partners testing the integration.
Integration work happens in conversations as much as in code: working sessions with the provider’s team, and days at the office with the people building alongside me.

