Mask PII in Public API Responses by Default
The API response is the exposure surface, not the rendered page. Mask email, phone, and references at serialization; gate full data to admins.
Your API returns a user object. It has a name, an email, a phone number, and somewhere in there, because the same database row holds everything, a payment reference and maybe an internal id. The frontend only displays the name and a masked email. But the API returned the whole object, and anyone who opens the browser's network tab, or hits the endpoint directly, sees every field, including the ones the UI never shows. The data was never on screen, but it was always in the response, sitting in plain JSON for anyone curious enough to look.
This is one of the most common and most overlooked data leaks in modern applications. Nothing was hacked. The API simply returned more than it should have, because it serialized the whole database object and trusted the frontend to display only the safe parts. The fix is to decide what each response is allowed to contain based on who is asking, and to mask sensitive fields by default rather than relying on the client to hide them. The data the API does not send cannot leak.
The leak nobody notices
The pattern that causes this is innocent and everywhere. A developer writes an endpoint that fetches a user record and returns it. The ORM hands back the full object, the framework serializes it to JSON, and the response goes out the door with every column the database holds. The frontend picks the three fields it needs and ignores the rest. Everything looks fine in the UI.
But the response is the API's output, not the UI's. Whatever is in the JSON is exposed to anyone who can call the endpoint, regardless of what the rendered page shows. The email the UI masks to j***@example.com is in the response as the full address. The phone number the UI never displays is right there. The payment reference, the internal account id, the fields that exist only for backend logic, all of it ships to the client because nobody decided what the response was allowed to include. This is why protecting PII is no longer optional, and why regulations like GDPR and CCPA treat over-exposure in responses as a real violation, not a theoretical one, the kind of cost that compounds when security gets skipped early.
Mask by default, in the response layer
The principle that fixes this is to mask sensitive fields by default, at the point where the API builds its response, not at the point where the UI renders. The response layer is the right place because it is the boundary the data crosses on its way out, and a field masked there is masked for every consumer, the browser, the mobile app, the direct API caller, the curious attacker.
The fields that need masking in public-facing responses are the predictable ones:
- Email addresses, shown as
j***@example.comrather than the full address. - Phone numbers, shown as the last few digits only.
- Payment references and tokens, which should essentially never appear in a public response at all.
- Internal ids and any backend-only field that the client has no business seeing.
The masking itself uses standard techniques. Character masking preserves the shape while hiding the content, turning a phone number into a string of asterisks with a few real digits, so the UI can still show a recognizable hint without exposing the full value. Redaction replaces a field entirely with a placeholder when even a hint is too much. The technique matters less than the rule: the response layer masks these fields by default, and exposing the full value is the deliberate exception that requires justification, not the accident that happens because nobody thought about it.
Build masking into serialization, not into every handler
The way to make this reliable rather than something a developer has to remember on every endpoint is to enforce it in the serialization layer. You annotate the sensitive fields once, on the model, and a serializer masks them automatically wherever that model is returned. The developer cannot forget to mask the email, because masking is the default behavior of the field, applied everywhere the object is serialized.
This is the difference between "we mask PII on most endpoints" and "PII cannot be exposed unless someone explicitly chooses to expose it." The first is a policy people violate by accident; the second is a property the system enforces. When the masking lives on the field definition, adding a new endpoint that returns the user object gets the masking for free, and the leak from the opening of this post simply cannot happen, because the full email is never serialized in the first place.
Admin views need the real data, and that is fine
The obvious objection is that admins legitimately need to see the full email, the real phone number, the actual payment reference, to do their jobs. That is true, and the solution is not to abandon masking. It is to make unmasked data a separate, authorized path.
When sensitive data genuinely needs to be displayed, an admin dashboard showing a customer's full details, the unmasking is handled through a secure, authenticated request that checks the caller's authorization before returning the real values, gated by scoped API tokens so a leak cannot touch everything. The public response masks by default; the admin path returns full data only after verifying the caller is actually an admin, often with an additional authorization check for the most sensitive fields, and every unmasking should leave a trail in audit logs that actually help after a breach. The two paths are distinct, and the privileged one is gated.
This keeps the rule clean. Public and unauthenticated responses mask. Authorized admin responses, behind proper authentication and authorization, can unmask the specific fields that role is permitted to see. The default is safe, and the exception is explicit and access-controlled, which is exactly the posture you want. An admin seeing full data is a deliberate, authorized action; a public endpoint leaking it is the bug this design eliminates.
One more rule: payment references never go public
Among the fields to handle, payment references, transaction tokens, and anything tied to a payment processor deserve a stricter rule than masking. These should not appear in public API responses at all, masked or otherwise, because they can be leveraged to look up or correlate transactions and they have no legitimate reason to be on the client. The same goes for any token or reference that exists to authenticate a backend operation. Mask emails and phones so the UI can show hints; omit payment references and internal tokens entirely from anything a non-admin can reach. The same restraint about what a response confirms is what underpins preventing user enumeration in your login and reset flows, where even acknowledging that an email exists is a leak.
Where this fits in a secure build
Over-exposure in API responses is a quiet vulnerability because it never announces itself. The app works, the UI looks correct, and the leak only surfaces when someone opens the network tab or a researcher reports it, or worse, when a scraper has been harvesting full email lists from your "public" profile endpoint for months. It is the same class of quiet exposure as shipping API keys in your frontend bundle: invisible from the front end and obvious from the wire. It is exactly the kind of issue our security audits check for, and the gap between those two views is where the data quietly walks out.
We build the masking-by-default posture into every web app we ship: sensitive fields masked at the serialization layer, full data available only through authenticated and authorized admin paths, payment references and internal tokens kept out of public responses entirely. The result is an API where the response contains exactly what the caller is allowed to see, and the data you did not intend to expose is data that was never serialized to send.
The rule reduces to one sentence: the API response is the exposure surface, not the rendered page, so decide what each response is allowed to contain and mask the rest by default. Do that and the full email the UI never showed stops being one network-tab click away from anyone who wants it.






