Every engineering team that works with external APIs eventually needs test data that looks real. The temptation to grab a production response and scrub the obvious fields is understandable: it’s fast, the shape is exactly right, and the test passes immediately. The problem is that “scrub the obvious fields” is harder than it looks, and the consequences of getting it wrong sit in version control for years.
This guide covers what actually counts as PII in API payloads (including the cases teams routinely miss), the trade-offs between redaction, tokenization, and shape-preserving masking, and why masking before data leaves your own network is the only approach that holds up.
What counts as PII in API payloads
The obvious cases are well understood: names, email addresses, phone numbers, postal addresses, payment card numbers, government ID numbers. Most teams know to strip these. The harder cases are what bite you.
Sequential identifiers. A user ID like 10042837 looks harmless in isolation. In a fixture file that also carries a timestamp, a plan type, and an account creation date, it’s a handle that a motivated person can use to correlate records across systems. GDPR Art. 4 defines personal data as anything relating to an identifiable natural person, not just data that identifies them on its own. A sequential ID in context often clears that bar.
Timestamps and date precision. An account created at 2024-03-14T09:23:41Z is not just a date. It’s a behavioral data point. Combined with other fields, precise timestamps can narrow down who a record belongs to. ISO 8601 timestamps in test fixtures are rarely necessary at sub-day precision for most test purposes.
Free-text and notes fields. Fields named notes, description, comment, memo, or instructions are frequently overlooked by automated masking rules because they don’t match a named-field pattern. Yet these are exactly the fields where support agents and users type full names, addresses, and case-sensitive personal details in natural language. Regex-based PII detection in free text is unreliable; the safer rule is to treat every free-text field as sensitive by default.
Geolocation data. A precise lat/lon pair associated with a user record can identify a home or workplace, even if the user’s name is stripped. Even city-level location tied to a small user cohort can re-identify individuals. The GDPR’s Recital 26 on anonymisation specifically addresses combination attacks: data isn’t anonymous just because one field is removed if the remaining fields allow re-identification.
Combinations that re-identify. This is the most common underestimation. Age bracket + zip code + gender identifies the majority of individuals in most populations in academic literature. In API payloads, the analogous combination is usually: account tier + signup date (week precision) + locale + a behavioral field. None of those fields is PII by itself. Together, they often are. Before declaring a payload scrubbed, look at what remains as a set, not field by field.
Derived fields. A credit score, a fraud risk score, or a churn probability derived from personal data is itself personal data under most interpretations of the GDPR. Derived fields often travel under innocuous-looking names like score, rating, or likelihood.
Redaction, tokenization, and shape-preserving masking
Once you’ve identified what needs to go, there are three broad approaches to replacing it.
Redaction is deletion: the field is removed from the payload, or its value replaced with null or an empty string. It’s simple and deterministic. The problem is that many tests depend on the field being present: validators check for required fields, parsers allocate memory for expected keys, snapshot tests will diff the before and after. A redacted fixture often breaks the test you were trying to enable.
Consider a payment response with a billing_address object. Redacting it produces a structurally different payload. If your code path branches on whether billing_address is present, your test now exercises the absent branch, not the present one.
Tokenization replaces a sensitive value with a consistent pseudonym: the same input always produces the same token. This is useful when you need referential consistency across records: a user ID that appears in multiple fixtures should map to the same token everywhere. The downside is that tokens don’t preserve type or format: a tokenized email address is usually not a valid email address, a tokenized phone number doesn’t pass a phone validator, and a tokenized UUID doesn’t look like a UUID. That breaks field-level validation, regex checks, and any downstream component that parses the value.
Shape-preserving masking replaces the value with a placeholder of the same type. By default: strings become "<masked>", numbers become 0, booleans become false. With the opt-in format-preserving mode, string placeholders retain their format shape: a masked email is still a syntactically valid email address (at a placeholder domain), a masked UUID is still RFC 4122-shaped, a masked IBAN carries a correct mod-97 checksum. The field remains present, its type is preserved, validators and parsers continue to work, and snapshot diffs stay stable.
Shape preservation is what test fixtures actually need. The downstream systems consuming your API responses (your own validation middleware, your type parsers, your third-party integrations) were built to work with well-formed values. A fixture that breaks them is a fixture that doesn’t test what you think it tests.
Why schema-valid shapes matter for tests
The consequence of shape-breaking masking shows up in three distinct ways.
Validators fail on the fixture instead of on your code. If you redact a required field, your validation layer throws before your business logic runs. You’re testing error handling, not the happy path you intended to test. The test passes for the wrong reason or fails for the wrong reason, and either way you’ve lost signal.
Snapshot diffs become noisy. Snapshot tests compare serialized output. If your masking changes the structure of the payload, say a field disappears or a type changes from number to null, the snapshot diff fires on every test run that touches that fixture, regardless of whether your code changed. Teams learn to ignore noisy diffs. Ignored diffs miss real regressions.
Integration points break silently. APIs you call may themselves make downstream calls based on the response shape. A payment processor might parse billing_address.country_code to apply tax logic. If your masked fixture has "country_code": null instead of "country_code": "NL", the test exercises a different code path than production traffic does.
Where to mask: at the edge, not in a pipeline
There’s an architectural question behind masking: at what point in the data flow do you apply it?
The answer matters because data that has crossed a network boundary before masking has already been disclosed. If you capture raw API responses, store them somewhere, and then apply masking rules before handing fixtures to developers, the raw personal data has already left your production environment and been processed in a second system. That processing requires its own lawful basis. The people with access to the storage layer have access to cleartext personal data. An incident in that storage layer exposes real personal data, not masked data.
The alternative is to apply masking rules at the capture point, inside your own infrastructure, before any payload is transmitted. The sequence looks like this:
- Your service handles a request normally.
- A middleware intercepts the request/response pair.
- Masking rules execute in-process: sensitive field values are replaced before the payload object is serialized.
- Only the masked payload is sent anywhere outside your network boundary.
The practical consequence: what leaves your infrastructure is never cleartext personal data. The structural shadow (field names, nesting, types, edge-case shapes) is sufficient for generating test fixtures. The values that would identify someone are not present.
This design also fail-closes more cleanly. If the masking configuration for a field is missing or the field path doesn’t match, the default behavior should be to treat the field as sensitive and mask it, not to pass it through. An opt-in masking model (mask what you explicitly list) leaves unlisted fields exposed. An opt-out model (mask everything, allowlist what’s safe) is harder to configure but safer by default.
A practical checklist
Before committing a fixture file derived from real API traffic, work through this list.
- Named PII fields scrubbed. Names, emails, phone numbers, addresses, payment card data, government IDs.
- Identifiers evaluated in context. Sequential IDs reviewed for re-identification risk when combined with other fields in the same fixture.
- Timestamps truncated or replaced. Sub-day precision stripped unless the test specifically requires it.
- Free-text fields masked. Any field typed as an arbitrary string (notes, comments, descriptions) treated as potentially containing sensitive content.
- Geolocation reviewed. Precise lat/lon replaced; city or region kept only if coarse enough not to re-identify.
- Derived fields checked. Risk scores, computed attributes, model outputs derived from personal data masked.
- Shape preserved. Every masked field still has the correct type; required fields are present; values pass field-level validation.
- Consistency across fixtures. If the same entity appears in multiple fixtures, its masked values are consistently shaped (though they don’t need to be consistent across test runs unless referential integrity matters to the test).
- Masking applied before data left your network. If you can’t confirm this, the fixture should not be committed regardless of how thoroughly you’ve scrubbed it.
Where Stubsmith fits
Stubsmith’s capture model is built around the edge-masking principle. An Express middleware intercepts API traffic in your own infrastructure, applies your masking configuration before serialization, and transmits only the masked payload. Stubsmith’s servers receive masked bodies plus field and path names: no raw field values. Fingerprint deduplication groups structurally identical traffic shapes so you accumulate representative fixtures without storing thousands of near-identical copies.
If you’re evaluating whether that approach fits your workflow, the free plan covers most solo and small-team setups, and the full SDK documentation is at docs.stubsmith.dev.
The checklist above applies regardless of what tool you use. The invariant that matters is this: masking happens before data crosses a network boundary. That is a pipeline design decision, not a product decision.