If you are integrating Aadhaar masking into a KYC pipeline, the API call itself is the easy part. It is one multipart POST. You will have it working in twenty minutes.
The hard part is everything around it: knowing when masking silently failed, deciding what to do with documents the OCR cannot read, and sizing throughput so a batch job at month-end does not time out. This guide covers the call, and then the parts that actually cause incidents.
What this covers
- The minimum working request
- The one response header that matters
- Handling the documents that fail
- Throughput, timeouts and batch jobs
- What to check before you go live
The minimum working request
A masking API should take a document and give you back the same document with the number redacted. No job IDs, no polling, no webhook to configure. That is what this one does:
curl -X POST https://api.maskaadhaar.com/api/v1/mask-aadhaar \ -H "X-API-Key: $MASKAADHAAR_KEY" \ -F "file=@aadhaar.jpg" \ --output aadhaar_masked.jpg
The response body is the masked file. In Python:
import os
import requests
with open("aadhaar.jpg", "rb") as fh:
response = requests.post(
"https://api.maskaadhaar.com/api/v1/mask-aadhaar",
headers={"X-API-Key": os.environ["MASKAADHAAR_KEY"]},
files={"file": ("aadhaar.jpg", fh, "image/jpeg")},
timeout=120,
)
response.raise_for_status()
with open("aadhaar_masked.jpg", "wb") as out:
out.write(response.content)
That code works. It is also, as written, unsafe to put in production. Here is why.
The one response header that matters
Every masking API will happily return you a document it did not change. OCR is probabilistic. If the photograph is blurry, if the card is behind a plastic sleeve reflecting a ceiling light, if the number sits over a fold in the paper, the engine returns nothing and the service has a choice: fail the request, or return the original.
Returning the original is usually the right behaviour, because a hard failure on a legitimate document breaks the customer's onboarding flow. But it means a 200 response does not mean the document was masked.
The count is in a header:
masked_count = int(response.headers.get("X-Masked-Count", 0))
if masked_count == 0:
# The document came back unchanged. It still contains a live Aadhaar number.
raise DocumentNotMasked(f"No Aadhaar number found in {filename}")
Treat zero as a failure. Route those documents to a manual queue, or reject the upload and ask the customer for a clearer photograph. What you must not do is write the response body into the same bucket as your masked documents, because you will have stored an unmasked Aadhaar number under a filename that says otherwise. That is the failure mode auditors find.
The dangerous outcome is not an error. It is a success response carrying an unmasked document.
Handling the documents that fail
In our own testing, the documents that fail cluster into a few recognisable groups, and each wants a different response from your pipeline.
Glare and reflection
A laminated card photographed under an office light produces a bright band across the number. OCR reads part of it, or reads it with a character substituted — a zero becomes the letter O, an eight becomes a B. A good masking service repairs these; every service has a limit. Retrying the same file will not help. Ask for a new photograph.
Creases and folds
A printed e-Aadhaar folded through the number is close to unrecoverable. The digits either side of the crease are physically destroyed in the image. No amount of preprocessing invents them back.
Two-sided printouts
This is the interesting one, and it is worth understanding because it is where masking systems fail silently rather than loudly.
UIDAI's standard e-Aadhaar printout puts both sides of the card on a single page. Sometimes they sit side by side, both upright. Sometimes, particularly when someone has scanned a physical printout, one card is rotated ninety degrees relative to the other.
A naive detector scans the page, finds the number on the first card, and stops. It has found an Aadhaar number, after all. The second card — rotated, still carrying the same number in large print — is never examined. The service reports success. The document goes into your store with the number plainly visible.
If you are evaluating masking vendors, this is the single best test to run: take a real two-sided e-Aadhaar scan where the cards are at different orientations, and check the output with your own eyes. Do not check the status code. Look at the image.
Throughput, timeouts and batch jobs
OCR is CPU-bound. Masking one Aadhaar image is not a millisecond operation — it involves rendering, preprocessing, and several passes of a text recognition engine across the page at different orientations. Expect low single-digit seconds per document on properly provisioned hardware, and longer for multi-page PDFs where every page is scanned.
Three consequences for your integration:
Set a long timeout. The default in most HTTP clients is somewhere between ten and thirty seconds. A multi-page PDF can exceed that. Ninety to a hundred and twenty seconds is a sensible ceiling. A timeout on the client side while the server is still working wastes the document against your quota and gives you nothing.
Bound your concurrency. If you fire two hundred documents at the API simultaneously from a batch job, you will hit the per-minute rate limit and start collecting 429s. Use a small worker pool — four to eight concurrent requests is usually right — and let it drain the queue steadily.
Distinguish the two kinds of 429. They are not the same and retrying is only correct for one of them:
| Error | Meaning | What to do |
|---|---|---|
rate_limited | Too many requests this minute | Back off exponentially and retry |
quota_exceeded | Monthly document allowance used up | Stop. Retrying will not help. Alert someone. |
Branch on the error field in the JSON body, not on the status code alone, and not on the human-readable message, which can change.
if response.status_code == 429:
err = response.json().get("error")
if err == "rate_limited":
backoff_and_retry()
elif err == "quota_exceeded":
alert_ops("Monthly masking quota exhausted")
raise QuotaExhausted()
A word on false positives
Over-masking is a real problem and it is less discussed than under-masking, because it fails visibly rather than dangerously. Still, it produces documents your operations team has to deal with.
An Aadhaar number is twelve digits. An e-Aadhaar page contains a lot of other digits: a sixteen-digit Virtual ID printed directly below the number, a date of birth, an enrolment number, a PIN code, the UIDAI helpline number. A detector that looks for "twelve digits in a row" will find several, because the VID alone contains multiple twelve-digit windows.
The defence is that Aadhaar numbers carry a Verhoeff checksum. A randomly chosen twelve-digit slice of a VID will fail it roughly nine times out of ten. Combined with the rule that UIDAI does not issue numbers beginning with 0 or 1, the false positive rate drops sharply.
When you are evaluating output, check that the VID line and the date of birth are still legible. If a vendor's masking blacks those out too, their detector is not validating checksums, which tells you something about what else it might be getting wrong.
What to check before you go live
A short list, drawn from the failure modes above:
- Your code reads
X-Masked-Countand treats zero as a failure, not a success - Documents that fail masking go to a manual queue, never to the masked-document store
- Your HTTP timeout is at least ninety seconds
- Batch concurrency is bounded, and 429s are handled with back-off
quota_exceededraises an alert rather than silently retrying- You have visually inspected the output for a two-sided e-Aadhaar scan with mixed orientations
- You have confirmed the VID and date of birth are not masked
- Your API key is in a secret store, not in the repository
The last one sounds obvious and is the most commonly violated. Keys belong in environment variables or a secrets manager. If a key does leak, it should be revocable in one step — which is a question worth asking your vendor before you sign.
Where to start
If you want to try the masking behaviour before writing any integration code, the browser-based Aadhaar masking tool runs the same detection logic entirely on your own machine, with no upload. Drop in one of your own difficult documents — a folded printout, a two-sided scan — and see what comes back.
When you are ready to integrate, the API documentation has the full endpoint reference, error codes and examples in cURL, Python and Node.js. For volume pricing and a test quota, get in touch with your expected monthly document count and peak throughput.
Frequently asked questions
Does a 200 response mean my document was masked?
No. If the API cannot find an Aadhaar number it returns the original document unchanged with a 200 status. Always read the X-Masked-Count response header and treat a value of zero as a failure that needs manual review.
How long does one Aadhaar document take to mask?
Expect low single-digit seconds for a single image on properly provisioned hardware. Multi-page PDFs take longer because every page is scanned separately. Set your HTTP client timeout to at least ninety seconds to allow for the slowest documents.
What happens if the Aadhaar number is unreadable?
The document is returned unchanged with X-Masked-Count set to zero. Common causes are glare on a laminated card, a fold through the number, or a very low resolution photograph. Retrying the same file will not help; request a clearer image from the customer.
Will the API mask the Virtual ID or date of birth by mistake?
It should not. Candidate numbers are validated against the Verhoeff checksum that Aadhaar uses, which rejects the twelve-digit windows found inside a sixteen-digit VID as well as digit sequences formed from dates. If a masking tool blacks out the VID or date of birth, it is not validating checksums.
Can the API handle both sides of an Aadhaar card on one page?
Yes, including the common layout where one card is rotated ninety degrees relative to the other. This is worth testing explicitly when evaluating any masking service, because a detector that stops at the first number it finds will mask one card and leave the other fully visible while still reporting success.