Short answer: AICredits documents a server-side guardrail that replaces emails, phone numbers, Aadhaar, PAN, card numbers, bank account numbers and passport numbers with placeholders before a request reaches the model provider. It is enabled on the server side, not per request, so test it with fake data before you rely on it, and keep your own redaction in front of it.
Disclosure: the aicredits.in links are affiliate links and I use the service. The masking behaviour below comes from the AICredits docs as read on 16 Oct 2026. I have not run the test script in this post against the live API, so treat its output as something you will observe, not something I am reporting.
A support bot is the classic place this goes wrong. A customer types "my PAN is ABCDE1234F, please fix my KYC" and your code forwards the whole message to a model API. The identifier now sits in someone else's infrastructure and probably in your own request logs. You can stop that at three points: in your app, at the gateway, or never collect it in the first place. This post is about the middle one, and why it should not be the only one.
What does AICredits PII masking actually mask?
The guardrails page lists seven categories. When masking is on, request content is scanned and matches are replaced before the call to the provider:
| Category | Docs example | Placeholder |
|---|---|---|
| Email address | user@example.com | [EMAIL] |
| Phone number | +91-9876543210 | [PHONE] |
| Aadhaar number | 1234 5678 9012 | [AADHAAR] |
| PAN | ABCDE1234F | [PAN] |
| Credit card | 4111 1111 1111 1111 | [CREDIT_CARD] |
| Bank account | 9 to 18 digit sequences | [BANK_ACCOUNT] |
| Passport | "standard formats" | [PASSPORT] |
Three things stand out when you read it as an engineer rather than a buyer.
First, it is pattern based. Every category is something with a recognisable shape. Names, street addresses, dates of birth, medical details and free-text descriptions like "the man in flat 4B" have no shape, and they are not on the list.
Second, the bank account rule is broad by description. A "9 to 18 digit sequence" is also what an order number, a tracking ID or a long invoice reference can look like. The docs do not say how the matcher separates them, so if your prompts carry long numeric IDs, test whether they come back as [BANK_ACCOUNT].
Third, the docs describe masking of request content. They say nothing about restoring the original values in the reply, and I would not build a flow that expects it. If you need to put real values back, do the redact-and-restore step in your own code, as in PII redaction before LLM calls.
How is masking enabled?
Here the docs are plain: PII masking is toggled through a server-side environment variable, and you are told to contact support or check your account settings to enable it for your organization. No client change is needed. The related controls on the same page are blocked keywords (a match returns HTTP 400 and the request never reaches the provider, matching is case-insensitive across system, user and assistant turns) and response healing for malformed JSON.
What the docs do not describe is a per-request switch or a dashboard toggle you can flip yourself. So do not tell your security reviewer "we turned it on" until you have seen it work for the key you actually deploy. The next section is how.
How do you test masking safely with fake data?
Rules for the test: invented values only, a key with a small INR budget, and a cheap model. If masking is off or misses a pattern, the string goes to the provider, which is why the values must be fake.
The strings below are format-correct inventions. The Aadhaar, PAN, phone and card values are the same examples the AICredits docs use, and 4111 1111 1111 1111 is the widely published payment-test card number. None belongs to a person.
import os
from openai import OpenAI
# Untested: written from the AICredits docs, not run against the live API.
client = OpenAI(
api_key=os.environ["AICREDITS_API_KEY"],
base_url="https://api.aicredits.in/v1",
)
MODEL = os.environ.get("TEST_MODEL", "openai/gpt-4o-mini")
# (label, text sent, raw value we look for, placeholder the docs promise)
CASES = [
("email", "Contact me at user@example.com", "user@example.com", "[EMAIL]"),
("phone", "Call +91-9876543210 today", "9876543210", "[PHONE]"),
("aadhaar", "My Aadhaar is 1234 5678 9012", "5678 9012", "[AADHAAR]"),
("pan", "PAN: ABCDE1234F", "ABCDE1234F", "[PAN]"),
("card", "Card 4111 1111 1111 1111", "4111 1111", "[CREDIT_CARD]"),
# Variants that stress the matcher. Record what happens, do not assume.
("aadhaar-no-space", "Aadhaar 123456789012", "123456789012", "[AADHAAR]"),
("aadhaar-dashes", "Aadhaar 1234-5678-9012", "5678-9012", "[AADHAAR]"),
("pan-lowercase", "pan abcde1234f", "abcde1234f", "[PAN]"),
("order-id", "Order 20261016123456789", "20261016123456789", "(none expected)"),
("name", "Customer is Ramesh Kulkarni of Pune", "Ramesh Kulkarni", "(none expected)"),
]
SYSTEM = "Repeat the user's message back word for word. Add nothing else."
def run() -> None:
for label, text, raw, placeholder in CASES:
reply = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": text},
],
max_tokens=60,
temperature=0,
).choices[0].message.content or ""
if raw in reply:
verdict = "RAW REACHED MODEL"
elif placeholder in reply:
verdict = "masked"
else:
verdict = "UNCLEAR (model paraphrased or refused)"
print(f"{label:18} {verdict:38} {reply!r}")
if __name__ == "__main__":
run()
How to read the output:
- masked: the model echoed the placeholder, so it never saw the raw value.
- RAW REACHED MODEL: either masking is not enabled for this key, or the pattern missed. Run the plain
aadhaarcase first. If that one is raw, masking is simply off and the variants tell you nothing yet. - UNCLEAR: some models refuse to echo what looks like an ID. Change the system line to a neutral task, such as "Summarise this in five words", and check the usage log too.
This is an indirect test. The echo shows what the model saw, but not what your request logs stored. Read the data retention notes for that. The guardrails page says request and response content is stored for 30 days by default, while the features page describes 30-day retention for metadata, so ask support exactly what your account keeps.
Run it once with masking off (if you can get a baseline) and once with it on. The difference is your evidence.
Where regex masking stops
Pattern masking is good at the structured stuff and blind to the rest. Expect these gaps, and check each against your test run:
| Input | Why a pattern matcher struggles |
|---|---|
| "Ramesh Kulkarni, Flat 4B, Pune" | Names and addresses have no fixed shape |
| Aadhaar written as words, or with letters swapped for digits | No digit run to match |
| Lowercase or spaced PAN | Depends on how strict the pattern is |
| Digits inside an image or scanned PDF | The text layer never reaches the matcher |
| PII in tool results your agent fetches mid-run | Depends on whether that call passes through the masked path |
| A 12-digit order or SKU number | May be masked when it should not be, which breaks the prompt |
Two failure directions matter. A miss leaks data. A false positive corrupts the prompt: if a 14-digit reference becomes [BANK_ACCOUNT], the model can no longer look it up. Test with your own realistic ID shapes, not just the tidy examples.
I compared a regex redactor against Microsoft Presidio on invented Indian text in this earlier post, and the pattern is the same: structured IDs are catchable, names and places need a model, and nothing catches everything.
What is defense in depth for LLM calls?
Treat the gateway guardrail as one layer. A reasonable stack for an app that touches Indian identity data:
- Do not collect what you do not need. If the task is "explain this KYC rejection", the Aadhaar number is not an input. Strip it in the UI.
- Redact in your own code first. Mask or tokenise identifiers before the HTTP call, and keep the mapping on your side if you need to restore values.
- Keep the gateway masking on as the backstop, and verify it with the script above after any account or config change.
- Cap the blast radius. Use a dedicated key with an INR budget cap per app, so a leaking or looping integration is bounded. The Claude via INR guide shows the budget setup.
- Limit what you log. Your own application logs and traces are a bigger PII store than the model call. See the API key and log leak checklist.
- Check the output side. A model can repeat identifiers from retrieved documents, so scan replies too, and read the hallucinations lesson for why you should not trust a model to self-report what it saw.
- Defend the input channel. Masking does nothing about an attacker who plants instructions in a document your bot reads. The prompt injection lesson covers that separate problem.
- Decide where inference should run at all. For the most sensitive workloads, the local vs cloud privacy guide is the better starting point than any masking layer.
A note on the DPDP Act
India's Digital Personal Data Protection Act, 2023 puts obligations on how personal data is collected, used, stored and erased. A masking layer helps with minimisation and with the reasonable security safeguards a fiduciary is expected to keep. It does not cover consent notices, purpose limits, retention schedules, deletion requests or contracts with processors. I am an engineer writing about tooling, not a lawyer. Have counsel map your actual data flows, and keep the output of the test script as evidence of what you verified and when.
Quick decision checklist
- Support has confirmed masking is enabled for the exact key you ship with.
- The fake-data script ran with masking on, and you saved the output.
- You ran your own real ID shapes (order numbers, invoice IDs) to check for false positives.
- Names, addresses and dates of birth are handled in your code, because they are not on the documented list.
- The key has an INR budget cap and a name that says which app it belongs to.
- Application logs and traces do not store the raw text.
- A lawyer has seen the data flow if you handle identity documents at scale.
If you do not have an account yet, AICredits takes a minimum top-up of ₹50, and the full AICredits review covers what else the gateway does. Whatever you pick, run the test, save the output, and keep identifiers out of prompts wherever your design allows it.



