REST API · v1

Aadhaar & PAN Masking API

One HTTP request. Send a document, receive it back with the identity number redacted. Built for KYC pipelines at banks, NBFCs, housing finance companies and fintechs.

Access is issued manually. Email contact@maskaadhaar.com with your expected monthly volume and we will provision a key and a test quota.

Quick start

Every request is a single multipart/form-data POST carrying one file. The response body is the masked document — there is no JSON envelope to unwrap and no second call to fetch a result.

POST https://api.maskaadhaar.com/api/v1/mask-aadhaar
# Mask an Aadhaar card and write the result to disk
curl -X POST https://api.maskaadhaar.com/api/v1/mask-aadhaar \
  -H "X-API-Key: your_api_key" \
  -F "file=@aadhaar.jpg" \
  --output aadhaar_masked.jpg

Masking is applied to the first eight digits of the Aadhaar number, leaving the last four visible — the format UIDAI describes for shared copies.

Authentication

Pass your key in the X-API-Key header on every request. Keys look like mak_live_... and are issued per organisation.

X-API-Key: mak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Handling your key. Keys are stored securely and can be rotated or revoked at any time from your dashboard, where you can also read your current key back. Revocation takes effect immediately. Treat the key like a password: anyone holding it can spend your quota, so keep it server-side and out of client code and version control. If it leaks, rotate it from the dashboard and the old key stops working at once.

A missing key returns 401 missing_api_key; an unrecognised or revoked key returns 401 invalid_api_key.

Supported formats

InputContent typeNotes
JPEGimage/jpegPhotographs and scans
PNGimage/pngScreenshots and exports
PDFapplication/pdfMulti-page e-Aadhaar; every page is scanned

The optional output_format field accepts pdf, jpg, or same (the default, which keeps the input format where possible).

Mask Aadhaar

POST/api/v1/mask-aadhaar

Detects Aadhaar numbers in the document and redacts the first eight digits of each. Both sides of a two-sided printout are handled, including the common layout where one card is rotated ninety degrees relative to the other.

FieldTypeRequiredDescription
fileFileYesJPEG, PNG or PDF containing the Aadhaar card
output_formatStringNopdf, jpg or same (default)

Returns 200 with the masked file as a binary stream. If no Aadhaar number is found the original document is returned unchanged with X-Masked-Count: 0 — check that header rather than assuming success from the status code.

Validation. Candidate numbers are checked against the Verhoeff checksum that Aadhaar uses, so the sixteen-digit VID printed alongside the number, dates of birth and enrolment numbers are not mistaken for Aadhaar numbers and left alone.

Mask PAN

POST/api/v1/mask-pan

Detects PAN numbers and redacts the middle seven characters, leaving the first two and the last one visible: ABCDE1234F becomes ABxxxxxxxF.

FieldTypeRequiredDescription
fileFileYesJPEG, PNG or PDF containing the PAN card
output_formatStringNopdf, jpg or same (default)

Extract Aadhaar

POST/api/v1/extract-aadhaar

Reads an Aadhaar card and returns its fields as JSON. Same request shape as the masking endpoints — one multipart part named file — so an existing integration needs no new plumbing.

FieldTypeRequiredDescription
fileFileYesJPEG, PNG or PDF containing the Aadhaar card
include_confidenceBooleanNoAdds a per-field confidence object. Default false
reveal_aadhaar_numberBooleanNoRequests the unmasked number. Honoured only on an entitled key — see below
curl -X POST https://api.maskaadhaar.com/api/v1/extract-aadhaar \
  -H "X-API-Key: $MASKAADHAAR_KEY" \
  -F "file=@aadhaar.jpg"
{
  "success": true,
  "document_type": "aadhaar",
  "sides_detected": ["front", "back"],
  "fields": {
    "name": "Rahul Kumar",
    "aadhaar_number": "XXXX XXXX 9012",
    "aadhaar_last4": "9012",
    "date_of_birth": "1990-05-12",
    "year_of_birth": null,
    "gender": "M",
    "care_of": null,
    "address": {
      "full": "12 Anna Salai, Teynampet, Chennai, Tamil Nadu 600018",
      "pincode": "600018"
    },
    "vid_detected": false
  },
  "checksum_valid": true,
  "warnings": []
}
The number is masked by default. aadhaar_number comes back with the first eight digits covered, and aadhaar_last4 is supplied separately so you do not have to parse the mask off. The last four are what reconciliation against your own records needs; the first eight are what make the number a cross-database lookup key. If your use case genuinely requires the full number, the entitlement is granted per organisation against an attested lawful basis — talk to us. Requesting it on a key without the entitlement returns 403 reveal_not_entitled rather than silently downgrading to masked.

Three fields repay reading the notes on:

FieldWhat to watch
date_of_birthISO YYYY-MM-DD, or null. Older cards carry only a year: date_of_birth is then null and year_of_birth is set. We never synthesise a January date to fill the gap
addressReturns full and pincode. It does not split into line, locality, district and state: Aadhaar addresses are not consistently structured, and components that are wrong a fifth of the time are worse than one honest string because you cannot tell which fifth. A partial_address warning flags a read we were not confident about
checksum_validfalse means digits were read but do not form a valid Aadhaar number. Treat that as a read failure, not as a number

Warnings are non-fatal and returned as an array, empty when clean: low_contrast, single_side_only, rotated_input, partial_address, year_of_birth_only.

Extract PAN

POST/api/v1/extract-pan

Reads a PAN card and returns its fields as JSON.

FieldTypeRequiredDescription
fileFileYesJPEG, PNG or PDF containing the PAN card
include_confidenceBooleanNoAdds a per-field confidence object. Default false
{
  "success": true,
  "document_type": "pan",
  "fields": {
    "pan": "ABCPE1234F",
    "name": "RAHUL KUMAR",
    "father_name": "SURESH KUMAR",
    "date_of_birth": "1990-05-12",
    "holder_type": "individual"
  },
  "format_valid": true,
  "warnings": []
}

holder_type is decoded from the fourth character of the PAN itself, not from the card's QR code, which this endpoint does not read. P is an individual, C a company, H a Hindu Undivided Family, F a firm, T a trust, and so on. A character we have no label for returns null, which means "unrecognised code", not "invalid PAN" — rejecting a real PAN because our table is short would be the worse error.

PAN is returned in full. It is not governed by the Aadhaar Act, it carries no equivalent lookup-key property, and returning it is the norm for the category — so the masking default above does not apply here.

format_valid checks the structural rule only: five letters, four digits, one letter. It is a format check, not a validity check against the Income Tax department, and we do not claim otherwise — a perfectly well-formed PAN can belong to nobody.

holder_type decodes the fourth character, which encodes what kind of entity holds the PAN: individual, company, hindu_undivided_family, firm, trust and so on. It is reported, never enforced. null means we have no label for that character, not that the PAN is invalid — deliberately, because rejecting a real PAN over an unrecognised code would be a worse failure than reporting one we cannot name.

Verify document

POST/api/v1/verify-document

Checks whether an Aadhaar card's printed text agrees with the data UIDAI signed into its QR code. This is the one check on an Aadhaar card that can be proved rather than estimated: UIDAI signs the QR payload, so a card whose printed name disagrees with its signed name was altered, and a forger cannot re-sign the payload.

curl -X POST https://api.maskaadhaar.com/api/v1/verify-document \
  -H "X-API-Key: $MASKAADHAAR_KEY" \
  -F "file=@aadhaar.jpg"
{
  "verdict": "fail",
  "rationale": "The printed document disagrees with the data UIDAI signed.",
  "qr": { "status": "decoded" },
  "signature": { "status": "valid" },
  "checks_performed": [
    { "id": "qr_decoded",                  "result": true },
    { "id": "qr_signature_verified",       "result": true },
    { "id": "printed_vs_signed_crosscheck","result": true },
    { "id": "aadhaar_checksum",            "result": true }
  ],
  "signals": [
    { "id": "qr_name_mismatch", "severity": "high",
      "detail": "The printed name does not match the name in the signed QR." }
  ]
}
There is no tamper score, deliberately. A number that decides whether a real person gets a loan has to be calibrated against labelled genuine and forged documents in your own capture conditions. We do not have that corpus, and an uncalibrated score manufactures confidence with nothing behind it — a compliance officer cannot defend 0.83 at an audit. Named signals can be defended, acted on, and argued with.

Verdicts

VerdictMeaning
passThe QR decoded, UIDAI's signature verified, and every printed field matches what was signed.
reviewNothing contradicts the document, but something could not be established — no QR, an unreadable QR, or a signature we could not verify. A human should look.
failThe printed text disagrees with the signed payload, or the signature did not verify. This is a finding, not a probability.

Signals

SignalSeverityMeaning
qr_name_mismatchhighPrinted name differs from the signed name
qr_dob_mismatchhighPrinted date of birth differs from the signed one
qr_aadhaar_last4_mismatchhighPrinted last four digits differ from the signed reference. Works on a masked card, since the last four remain visible
qr_gender_mismatchmediumPrinted gender differs from the signed value
qr_pincode_mismatchmediumPrinted pincode differs from the signed address
aadhaar_checksum_invalidmediumTwelve digits that fail the Verhoeff check. Usually a misread on a poor capture rather than a forgery, which is why it reviews rather than fails
Resolution matters, and the response says so. A UIDAI Secure QR runs 117–177 modules a side, and no decoder recovers the payload below roughly three pixels per module — we measured 1.8 failing and 3.0 succeeding. A WhatsApp-compressed card photo typically sits under that. Where the QR is present but unreadable, the response returns px_per_module against the minimum, so you can tell a customer to re-photograph rather than guessing why it failed.

Values are never returned. The response says the printed name does not match the signed name; it does not return either name. Otherwise this would be an extraction endpoint by another route, handing back demographics you may have no basis to hold.

Signature verification needs UIDAI's certificate, provisioned per deployment. Without it the status is not_verified with the reason, never valid, and the verdict is capped at review however clean the cross-checks are — because a forger who replaces the whole QR with one encoding their own data produces a document that cross-checks perfectly. Talk to us about enabling it.

Redact PII

POST/api/v1/redact-pii

Covers identifiers on any document, without needing to know what the document is. The Aadhaar and PAN endpoints know what they are looking at; a KYC folder does not consist only of Aadhaar cards.

FieldTypeRequiredDescription
fileFileYesJPEG, PNG or PDF
typesStringNoComma-separated. Empty means the defaults below
TypeShapeOn by default
aadhaarTwelve digits, Verhoeff-validatedYes
vidSixteen-digit Virtual IDYes
panFive letters, four digits, one letterYes
passportOne letter, seven digitsYes
voter_epicThree letters, seven digitsYes
driving_licenceState code, RTO code, eleven digitsYes
gstinFifteen characters, embeds a PANYes
ifscFour letters, a zero, six charactersYes
phone_inIndian mobile, optional country codeYes
emailAnything shaped like an addressYes
bank_accountNine to eighteen digitsNo
Why bank_account is off by default. Nine to eighteen digits with no checksum describes an account number, an invoice number and a customer ID equally well. On by default it would quietly destroy the documents it was meant to make shareable, and unlike a missed number a wrong redaction cannot be undone. Enable it where you know the document type. GET /api/v1/pii-types returns this table as JSON, unauthenticated, so you can read it before a key is issued.
curl -X POST https://api.maskaadhaar.com/api/v1/redact-pii \
  -H "X-API-Key: $MASKAADHAAR_KEY" \
  -F "file=@document.pdf" \
  -F "types=aadhaar,pan,phone_in" \
  --output redacted.pdf

The response is the redacted document. X-Redacted-Count gives the number of regions covered and X-Redacted-Types a JSON object of counts by type. The values themselves are never returned in a header — a header carrying the identifier it just redacted would put that identifier into every access log between us and you.

PDFs are rasterised before redaction rather than annotated. Drawing a rectangle over a PDF leaves the text readable underneath by copy-paste or pdftotext, which is the failure this endpoint exists to prevent.

Bulk endpoints

POST/api/v1/bulk/mask-aadhaar
POST/api/v1/bulk/mask-pan
POST/api/v1/bulk/anonymise

Several documents in one request, returned as a ZIP. Attach each file as a repeated files part. Batch size is bounded per plan; ask us for yours.

curl -X POST https://api.maskaadhaar.com/api/v1/bulk/mask-aadhaar \
  -H "X-API-Key: $MASKAADHAAR_KEY" \
  -F "files=@a.pdf" -F "files=@b.jpg" -F "files=@c.png" \
  --output masked_batch.zip

The ZIP holds the processed documents and a manifest.json:

{
  "submitted": 3,
  "masked": 2,
  "no_number_found": 1,
  "failed": 0,
  "documents_charged": 2,
  "results": [
    {"index": 1, "filename": "a.pdf", "status": "masked",
     "masked_count": 2, "output": "001_a_masked.pdf"},
    {"index": 3, "filename": "c.png", "status": "no_number_found",
     "masked_count": 0, "output": "003_c_masked.jpg"}
  ]
}
no_number_found is a finding, not a pass. That document was processed and returned unchanged, so it is still readable. A batch reporting "48 documents processed" without separating those out has told you almost nothing, and those are precisely the documents that reach an archive still carrying a number. Branch on the status rather than on the count of files returned.

One document failing does not fail the batch — it gets a status and the rest are still returned. Quota is checked against the whole batch up front, so you are never left with a part-processed ZIP and a 429 halfway down the manifest. Charging follows the single-document rule: only documents that produced a redaction are billed.

Global documents

POST/api/v1/global/detect
POST/api/v1/global/redact
POST/api/v1/global/redact/text
POST/api/v1/global/batch/redact
GET/api/v1/global/entity-types

The endpoints above cover India. These cover documents from everywhere else: passports, bank statements, payslips, tax forms, medical letters and contracts from the US, UK, EU, Canada, Australia, Brazil and South Africa. You use the same key, the same quota and the same billing rule, and a call that finds nothing is not charged.

TypeCountryValidationOn by default
PASSPORT_MRZAnyICAO 9303 check digits, per field and compositeYes
PAYMENT_CARDAnyLuhnYes
IBANAnyISO 7064 mod-97 and country lengthYes
US_SSNUSSSA structural rulesYes
US_ITIN, US_EINUSFormatYes
US_ABA_ROUTINGUS3-7-1 weighted checksumYes
UK_NINOGBFormat and issued prefixesYes
UK_NHSGBmod-11Yes
CA_SINCALuhnYes
AU_TFN, AU_ABNAUWeighted mod-11 / mod-89Yes
BR_CPFBRDouble mod-11Yes
ZA_IDZALuhn and a valid birth dateYes
IN_AADHAAR, IN_PANINVerhoeff / formatYes
EU_VATEUFormatYes
EMAIL, PHONE, IP_ADDRESS, MAC_ADDRESS, CRYPTO_BTC, CRYPTO_ETHAnyFormatYes
SWIFT_BIC, DATE_OF_BIRTHAnyFormatNo
Checksums decide, not shapes. Nine digits in a row could be an SSN, an invoice number or a case reference. Every identifier that has a checksum has to pass it before it is redacted. A candidate that fails is left alone and listed under rejected_candidates with the reason, for example "area 666 is not issued by the SSA". That way you can see what was considered instead of wondering what was missed. GET /api/v1/global/entity-types returns this table as JSON without a key, and you can filter it with ?country=GB or ?category=financial.

Redact a document

FieldTypeRequiredDescription
fileFileYesPDF, PNG, JPEG, WEBP, TIFF or BMP
modeStringNobox (default), blur (images only), mask, token or pseudonym
entity_typesStringNoComma-separated. Empty means the defaults
countriesStringNoISO-2 codes, e.g. US,GB. Limits country-specific types to those countries; universal types still apply
mask_keepIntegerNoCharacters kept by mask mode, 0–8. Default 4
strip_metadataBooleanNoRemove PDF author/producer and image EXIF, including GPS. Default true
response_modeStringNobinary (default) streams the file. json returns it base64-encoded with the full report
curl -X POST https://api.maskaadhaar.com/api/v1/global/redact \
  -H "X-API-Key: $MASKAADHAAR_KEY" \
  -F "file=@bank_statement.pdf" \
  -F "countries=GB" \
  --output redacted.pdf

X-Entities-Found gives the number of identifiers redacted, and X-Entity-Types lists their types separated by commas. Values never appear in a header. PDF text is removed from the content stream, not covered. The page keeps its layout and the rest of the text stays searchable, but pdftotext and copy-paste recover nothing from the redacted regions, because nothing is left there to recover.

ModeOutput for 4111 1111 1111 1111Use it for
boxBlack box, text removedArchives, sharing externally
mask************1111Support teams who need to reconcile the last digits
token[PAYMENT_CARD_1]Sending a document to an LLM, where the model still needs to know a card was there
pseudonym4000 0000 0000 0002, a reserved stand-in in the same formatTest and staging data that has to keep parsing
blurGaussian blurImages only

Tokens and pseudonyms stay consistent within one document, so the same SSN gets the same token everywhere it appears. They are never consistent across documents, so two files cannot be linked through their tokens.

Detect without changing anything

/global/detect takes the same file, entity_types and countries fields and returns a report instead of a file. Use it to triage an upload before deciding whether to accept it.

{
  "entities": [
    {"type": "US_SSN", "value_preview": "*******6789", "page": 1,
     "confidence": 1.0, "validated": true, "validation": "ssa-structural"}
  ],
  "rejected_candidates": [
    {"type": "US_SSN", "reason": "area 666 is not issued by the SSA", "value_preview": "*******3456", "page": 2}
  ],
  "summary": {"US_SSN": 1},
  "file_type": "PDF",
  "metadata_present": ["author", "producer"],
  "quota": {"used": "142", "remaining": "858"}
}

Redact text

JSON in, JSON out. The default mode is token, which is the one that makes a support ticket or a chat transcript safe to send to a model.

curl -X POST https://api.maskaadhaar.com/api/v1/global/redact/text \
  -H "X-API-Key: $MASKAADHAAR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Card 4111 1111 1111 1111, NI number AB123456C"}'
# {"redacted_text": "Card [PAYMENT_CARD_1], NI number [UK_NINO_1]", ...}

Batch

/global/batch/redact takes repeated files parts plus the same options. It returns JSON with one entry per file, each holding status (redacted, nothing_found or error), the report, and the file as file_base64. One bad file does not fail the batch, and only files in which something was redacted are charged.

Scanned documents are OCR'd. A PDF page with no text layer is read with OCR, and the response includes an ocr block with the pages and confidence. That way "nothing was found" and "we could not read this page" are different answers. Up to 30 scanned pages are OCR'd per document; the response tells you if that limit was reached.

Health check

GET/health

Unauthenticated. Returns {"status":"ok"} when the service is accepting traffic. Suitable for uptime monitoring.

curl -fsS https://api.maskaadhaar.com/health
# {"status":"ok"}

It answers 200 whenever the service can serve requests, and reports nothing else — no version, no configuration, no dependency status. Point a monitor at it and alert on anything that is not a 200. Note that a healthy service still returns 402 or 429 to a key whose period has lapsed or whose quota is spent; those are answers about your account, not about the service, so do not wire them into an uptime alert.

Example: cURL

# Capture the response headers alongside the masked file
curl -X POST https://api.maskaadhaar.com/api/v1/mask-aadhaar \
  -H "X-API-Key: $MASKAADHAAR_KEY" \
  -F "file=@aadhaar_front.jpg" \
  -D headers.txt \
  --output aadhaar_masked.jpg

# How many regions were redacted?
grep -i X-Masked-Count headers.txt

Example: Python

import os
import requests

API_URL = "https://api.maskaadhaar.com/api/v1/mask-aadhaar"

with open("aadhaar.jpg", "rb") as fh:
    response = requests.post(
        API_URL,
        headers={"X-API-Key": os.environ["MASKAADHAAR_KEY"]},
        files={"file": ("aadhaar.jpg", fh, "image/jpeg")},
        timeout=120,
    )

response.raise_for_status()

masked_count = int(response.headers.get("X-Masked-Count", 0))
if masked_count == 0:
    raise ValueError("No Aadhaar number was found — do not treat this as masked")

with open("aadhaar_masked.jpg", "wb") as out:
    out.write(response.content)

print(f"Redacted {masked_count} region(s)")
print(f"Quota remaining: {response.headers.get('X-Quota-Remaining')}")
Set a generous timeout. A multi-page PDF runs OCR over every page and can take longer than a default HTTP client timeout allows. Ninety to one hundred and twenty seconds is a reasonable ceiling.

Example: Node.js

import fs from "node:fs";

const form = new FormData();
form.append(
  "file",
  new Blob([fs.readFileSync("aadhaar.jpg")], { type: "image/jpeg" }),
  "aadhaar.jpg"
);

const res = await fetch("https://api.maskaadhaar.com/api/v1/mask-aadhaar", {
  method: "POST",
  headers: { "X-API-Key": process.env.MASKAADHAAR_KEY },
  body: form,
});

if (!res.ok) {
  const err = await res.json();
  throw new Error(`${err.error}: ${err.message}`);
}

const maskedCount = Number(res.headers.get("x-masked-count") ?? 0);
if (maskedCount === 0) {
  throw new Error("No Aadhaar number found — do not treat this as masked");
}

fs.writeFileSync("aadhaar_masked.jpg", Buffer.from(await res.arrayBuffer()));
console.log(`Redacted ${maskedCount} region(s)`);

Response headers

HeaderDescription
X-Masked-CountNumber of regions redacted. Zero means nothing was found — treat that as a failure in your pipeline, not a success.
X-Extracted-FieldsNon-null fields returned by an extraction call. Zero means nothing was read — the extraction analogue of a masked count of zero, and worth branching on rather than assuming
X-Entities-FoundIdentifiers found by a global endpoint. Zero means nothing was found, and the call was not charged
X-Entity-TypesComma-separated types redacted by /global/redact, e.g. IBAN,US_SSN. Types only, never values
X-Quota-UsedDocuments consumed in the current billing month. On a subscription plan this counts API documents only; the free browser tools are not metered. The figure matches your dashboard.
X-Quota-RemainingDocuments left in the current billing month, against the same allowance
Content-DispositionSuggested filename, e.g. aadhaar_masked.jpg

Error codes

Errors return JSON with a stable machine-readable error field. Branch on that, not on the human-readable message, which may change.

{
  "error": "quota_exceeded",
  "message": "Monthly quota of 1000 documents reached.",
  "quota": { "limit": 1000, "used": 1000, "remaining": 0 }
}
HTTPerrorMeaning
401missing_api_keyNo X-API-Key header was sent
401invalid_api_keyKey not recognised or revoked
403key_disabledKey exists but has been disabled
413file_too_largeFile exceeds the 50 MB limit
422unsupported_file_typeNot a JPEG, PNG or PDF
429rate_limitedRequests-per-minute limit exceeded — back off and retry
403reveal_not_entitledFull Aadhaar number requested on a key without the entitlement
422no_document_detectedNothing recognisable as the requested document type. Not billed
429quota_exceededMonthly document quota reached
500processing_failedMasking failed internally; safe to retry once

On 429 rate_limited, retry with exponential back-off starting around two seconds. On 429 quota_exceeded, retrying will not help until the quota resets or is raised.

Rate limits and quotas

Two independent limits apply: a per-minute request rate and a monthly document quota. Both are enforced per API key, not per IP address, so requests from behind a shared NAT do not compete with each other.

LimitValueHow to read it at runtime
Monthly documentsProvisioned per organisation X-Quota-Used and X-Quota-Remaining on every response
Requests per minuteProvisioned to your peak 429 rate_limited once exceeded; back off and retry
Max file size50 MB per document 413 above it, and nothing is charged

There are no fixed tiers to pick from. Both the monthly quota and the per-minute rate are set on your key when it is issued, so the numbers that apply to you are your own rather than a published band. Tell us your expected volume and peak throughput and we will provision for it, and raise it later without a key change.

What counts against your quota

Quota is consumed when a document comes back redacted. Rejected requests cost nothing, and neither does a document in which no number could be found.

OutcomeCounts?
401 invalid or missing keyNo
413 file larger than 50 MBNo
422 unsupported file typeNo
429 rate limited or quota exhaustedNo
500 processing failedNo
422 no document detectedNo
200 with X-Masked-Count above zeroYes
200 with X-Extracted-Fields above zeroYes
200 with X-Entities-Found above zeroYes
200 with X-Masked-Count: 0No
A zero count is free, and still dangerous. When no number can be read, the OCR work is performed but you are not charged — the same rule the browser tools follow. What matters is that the response body is then the original document, unchanged. Read the header rather than assuming a 200 means the document is masked.

Each request is recorded with its endpoint, size, processing time and status code, so a disputed month can be reconciled line by line.

Data handling

Documents are processed in memory and streamed back in the response. We do not write them to a database or object store and we retain no copy once the request completes.

One detail worth stating precisely, because it matters for your own security review: the OCR engine writes a short-lived temporary file to local disk while a request is being processed, which is removed when the request finishes. So the accurate claim is that documents are not persisted, rather than that they never touch disk. Processing runs in the AWS Mumbai (ap-south-1) region.

See our privacy policy for the full statement.

Ready to integrate?

Tell us your expected monthly volume and peak throughput. We will size a plan, issue a key and give your team a test quota to validate output against your own documents.