Every vendor selling document fraud detection in India will show you a tamper score. Ask two questions about it — what corpus calibrated the threshold, and what a compliance officer says at an audit when asked to justify a rejection at 0.83 — and the conversation usually changes shape.
There is a check on an Aadhaar card that needs no threshold, no training data and no interpretation, and almost nobody sells it. This is what it is, why it works, and the three places it genuinely does not.
On this page
The problem with a tamper score
Image-forensic tamper detection is real engineering. Error level analysis finds regions saved a different number of times to their surroundings. That is what a pasted patch looks like. Quantisation history reveals re-encoding. Font metrics catch text set in something other than the original. None of that is fake.
The difficulty is what comes out: a number between zero and one, which is useless until somebody picks a threshold. Pick it too low and you reject genuine customers whose documents are merely badly photographed. Too high and you accept the forgeries you bought the system for.
Choosing that number honestly requires a labelled corpus: genuine and forged documents, in the capture conditions your own customers actually use, in enough volume for the error rates to mean something. Field officers photographing laminated cards at angles under tube lighting produce a very different distribution from clean desktop scans. A threshold calibrated on someone else's distribution is calibrated on nothing.
Very few vendors have that corpus for Indian documents, and fewer still will tell you what theirs was. The result is a number that looks like measurement and functions as decoration.
And the practical failure is not statistical, it is procedural. When a rejected applicant complains, or an auditor asks why an account was refused, "the model scored it 0.83" is not an answer anyone can defend. It cannot be explained to the customer, argued with, or reviewed.
What is actually inside the QR
Every Aadhaar card issued since 2018 carries a Secure QR code, and most people who have looked at one assume it is a link or a lookup reference. It is neither.
Decode it and you get a very large base-10 integer. Convert that to bytes, decompress it, and you find the holder's name, date of birth, gender and full address, followed by a photograph, and at the end a 256-byte block. That last block is an RSA signature, produced by UIDAI over everything preceding it.
The QR is not a pointer to the data. It is the data, signed.
One detail matters more than it first appears: the reference identifier at the front of the payload opens with the last four digits of the Aadhaar number. Those are exactly the digits a masked card still shows. So a card that has already been redacted can still be checked against its own QR — masking and verification do not conflict.
Why a forger cannot beat it
Consider what altering a card actually involves. Someone opens the image, edits the printed name, and saves it. The printed text now says one thing. The QR still says the other.
To make them agree, the forger would have to rebuild the payload with the new name and sign it. That requires UIDAI's private key. That is the whole basis of the check, and it is not a matter of how good the forgery looks.
This is a different kind of statement from a tamper score. Not "this document has properties associated with manipulation". Rather: "the name printed on this card is not the name UIDAI issued it with." That survives being read aloud in a meeting.
There is one way around it, and it is worth stating plainly because it determines how you should deploy this. A sufficiently motivated forger does not edit the card at all. They replace the QR entirely with one they generated, encoding their own altered data. Print that alongside matching text and the two agree perfectly.
Which is exactly why the signature check is not optional. Cross-checking printed text against the QR catches the lazy forgery, which is most of them. Verifying UIDAI's signature over the payload is what catches the deliberate one. A system doing only the first should say so rather than implying the second, and should not return a clean verdict it has not earned.
The constraint nobody mentions
Here is the part that will decide whether this works on your document population, and it is rarely in anyone's marketing.
A QR code is a grid of modules. A UIDAI Secure QR carries enough data to need 117 to 177 modules a side. To decode it, a reader needs roughly three image pixels per module. Below that the information is simply not in the file, and no amount of processing recovers it.
We measured it rather than assuming: at 1.8 pixels per module, decoding fails at every upscale and threshold we tried. At 3.0 it succeeds reliably.
Three pixels per module across 137 modules means the QR itself needs to occupy about 400 pixels. On a full Aadhaar card that implies an image around 2000 pixels wide.
Now consider how documents actually reach you. A customer photographs their card and sends it over WhatsApp, which re-encodes and downscales. A field agent uploads through an app that resizes before transmitting to save bandwidth. Either produces something around 1200 pixels wide. The QR then lands near 1.8 pixels per module, which is the density we measured as unreadable.
This does not make the check useless. It makes capture quality part of the design rather than an afterthought. Two things follow. Your upload path must stop downscaling images before they reach verification, which is often a one-line change that nobody has had a reason to make. And your verification response must distinguish "this document has no QR" from "this QR was too small to read". The first is a fact about the document. The second is a fixable capture problem you can put back to the customer immediately.
What it does not tell you
Three things, and a vendor who does not volunteer them is worth pressing.
It does not verify the person. A card that passes is internally consistent with what UIDAI signed. Whether the individual presenting it is its holder is authentication, which requires registration with UIDAI as an authentication or KYC user agency. Those are different capabilities with different regulatory bases. Conflating them in a product description is the easiest way to mislead a buyer who is not looking closely.
It does not cover older cards. Cards predating the Secure QR format carry a legacy XML payload or nothing. Those cannot be checked this way, and the honest output is "not checked", not "passed". A verification system that returns a clean result for a document it could not examine is worse than one that has no opinion.
It does not check the photograph. The payload contains one, but comparing it to the face on the card, or to a selfie, is face matching. That is a separate problem with its own threshold and its own corpus requirement, and one that turns a selfie into biometric information your organisation now holds.
If you are building this yourself
It is a few hundred lines, and worth knowing where the sharp edges are.
- Get the certificate first. Without UIDAI's public certificate you have cross-checking, not verification. Decide up front what your system reports when it cannot verify, and make sure that is not a pass.
- Name matching should be directional. Every token in the signed name must appear in the printed text; extra tokens in the printed read are noise, not evidence. Our first implementation compared both directions and flagged a genuine card, because OCR had picked up fragments of the QR sitting on the same line and appended them to the name. A false forgery signal costs a real customer their onboarding.
- Treat a checksum failure as a review, not a rejection. Twelve digits failing the Verhoeff check are far more often one misread digit on a poor photograph than an altered number. Failing outright rejects people for bad lighting.
- Do not return the demographic values. Report that the printed name disagrees with the signed name, not both names. Otherwise you have built an extraction endpoint and put a complete identity record into every log between you and the caller.
- Report the pixel density on failure. "Decode failed" sends an engineer looking for a bug. "1.8 pixels per module, minimum 3.0" sends them to the upload path, which is where the problem is.
None of this needs a data science team, a training run or a corpus. It needs a QR decoder, a signature check and the discipline to report only what was established. In a category where almost everything is a probability with a confident number attached, a check that returns a fact is worth disproportionately more than its complexity suggests.
The check, as an API call. POST /api/v1/verify-document decodes the Secure QR, verifies UIDAI's signature where a certificate is configured, and returns named signals with a PASS / REVIEW / FAIL verdict. No tamper score, and the demographic values are never returned — only whether they agree.