Engineering
How to build a privacy-first enrichment pipeline that scales
Design a data enrichment pipeline around data minimization: send only the fields you need, keep raw PII out of logs, timestamp results, and limit access.

Enrichment pipelines tend to grow by accident. Someone adds a breach check here, a gender lookup there, and a year later personal data is flowing through queues, logs and dashboards that nobody designed for it. This guide shows how to build the pipeline the other way around: start from data minimization and add only what each use case actually needs.
The examples use the Finowl API, but the principles apply to any enrichment step that touches personal data.
Start with the question, not the data
Every enrichment call should answer a specific question that a specific team cares about. Before you write any code, write those questions down.
- "Has this customer's email appeared in a known breach since we last checked?" belongs to the security team.
- "Roughly what share of our newsletter audience uses names typically associated with each gender?" belongs to marketing analytics, and it is an aggregate question.
- "Is this password already known from breach data?" belongs to the signup flow and should never leave it.
Writing the question down forces two useful decisions. First, it tells you the minimum input. Second, it tells you the minimum output you need to keep. A security team answering the first question needs breach names and dates, not the full breach description copied into every row of your CRM.
Map each question to the smallest input
Finowl's endpoints already take narrow inputs. The email breach check takes one field, email. The gender endpoints take a name, email or username, plus an optional country hint for names. That makes minimization straightforward, as long as you resist the temptation to ship whole records.
| Question | Endpoint | Minimum input |
|---|---|---|
| Email exposed in a breach? | POST /v1/breaches/email |
email |
| Password seen in breach data? | POST /v1/breaches/password |
password (transient) |
| Aggregate audience gender mix | POST /v1/gender/batch |
first name, optional country |
| Light personalization | POST /v1/gender/name |
first name, optional country |
If you only need a gender estimate from a first name, send the first name. You do not need to send a surname, a phone number or an internal customer ID. When you use the batch endpoint, use opaque item ids that map back to your records internally, rather than putting emails in the id field.
Build the pipeline in stages
A clean pipeline separates four concerns: selecting records, calling the API, deriving results, and storing them. Keeping these stages separate makes it much easier to audit what personal data each stage touches.
Stage 1: select and validate
Pull only the columns you need from your source system. Validate them before you spend credits: Finowl rejects emails with display names, quoted local parts, IP-address domains or more than 254 characters, and rejected requests are free, but catching them locally keeps your logs and error rates clean. Normalize as you go by trimming whitespace and lowercasing email addresses.
Stage 2: call the API with minimal payloads
Here is a small Python worker that checks one email and returns only the derived fields the security team asked for.
import os
import requests
from datetime import datetime, timezone
API = "https://api.finowl.co/v1/breaches/email"
HEADERS = {
"Authorization": f"Bearer {os.environ['FINOWL_API_KEY']}",
"Content-Type": "application/json",
}
def check_email(email: str) -> dict:
resp = requests.post(API, json={"email": email}, headers=HEADERS, timeout=30)
resp.raise_for_status()
body = resp.json()
# Keep only what the security team needs, not the full breach records.
breaches = [
{"name": b["Name"], "added": b["AddedDate"], "verified": b["IsVerified"]}
for b in body["breaches"]
]
return {
"breached": body["breached"],
"breaches": breaches,
"has_password_exposure": any(
"Passwords" in b["DataClasses"] for b in body["breaches"]
),
"checked_at": datetime.now(timezone.utc).isoformat(),
}Notice what is not stored: the Description, PwnCount and other breach metadata. That information is useful when a human investigates, and they can fetch it on demand. It does not need to live next to every customer record.
Stage 3: derive, then discard
The derived result, for example has_password_exposure: true, is usually far less sensitive than the raw response and far more useful. Compute it in memory, write it, and let the raw response go out of scope. The same applies to gender predictions: for aggregate analytics you may only need counts per segment, so you can aggregate in the job itself and never write per-person predictions at all.
Stage 4: store with timestamps and provenance
Every enriched value should carry when it was produced and where it came from. A breach flag from eight months ago means something different from one produced this morning. For gender predictions, keep result_status, confidence and source so downstream consumers can tell a high-confidence dataset result from an AI-derived one or an unknown.
Keep raw PII out of logs
Logs are the most common place where personal data leaks out of a well-designed system. They are copied to third-party log tools, retained for months and readable by many engineers.
What to log instead
- Log a request id, endpoint, HTTP status, error code and latency.
- Log a salted hash or internal record id instead of the email or name.
- Log
breached: true/falsecounts per job, not per address.
Never log request bodies for enrichment calls. This matters most for the password endpoint. Finowl hashes the password and looks up only a 5-character hash prefix, and the password is processed transiently, never logged, stored or returned. Your own application should match that standard: no request body logging, no debug dumps and no error trackers capturing the payload. The password checks at signup guide and our post on k-anonymity for password breach checks cover this in more detail.
Errors are logs too
Error handlers often serialize the full request for debugging. Make sure your HTTP client wrapper strips bodies before an exception reaches your error tracker. Finowl error bodies are short, for example { "error": { "code": "invalid_input" } }, so logging the error code gives you everything you need to debug without the input.
Retention: decide how long each field lives
Enriched data should have an expiry date, chosen per field and per purpose.
- Breach flags go stale as new breaches are added. Re-check on a schedule and overwrite the old value rather than accumulating history you never read.
- Gender predictions for personalization can be dropped once the user states a preference, because the user's own answer always wins.
- Aggregate reports rarely need per-person inputs at all. Keep the aggregates and delete the row-level data once the report is built.
Implement retention as code, not as a policy document. A nightly job that deletes or nulls fields where checked_at is older than your limit is easy to write and easy to audit.
Access control for enriched data
Enriched fields can be more sensitive than the original record. A list of customers whose credentials appeared in breaches is valuable to attackers. Treat it accordingly.
Separate by purpose
Store security-related enrichment in a location the security team controls, not in the general CRM where every sales rep can export it. Expose marketing analytics as aggregates through your BI tool instead of granting row-level access.
Protect the API key
Your Finowl API key can spend credits and query personal data, so keep it in a secrets manager and inject it as FINOWL_API_KEY at runtime. Use separate keys per environment from the Finowl dashboard, and rotate them when people leave.
Tell people what you are doing
Email addresses and names are personal data, and enriching them is processing that your privacy notice should describe. Explain in plain language what you check, why, and how long you keep the results. For gender predictions in particular, mention that you use name-based estimates, that they are statistical and can be wrong, and that people can state or correct their own gender at any time.
Responsible use matters here too. A gender prediction is an estimate of how a name is typically used, not how a person identifies. Use it for aggregate analytics and low-stakes personalization only, never for eligibility, pricing, access or decisions with significant effects. Our post on responsible gender prediction walks through practical guardrails, and the data privacy concept page explains how Finowl handles the data you send.
A minimal checklist
Before you ship an enrichment job, confirm that:
- Each enrichment answers a documented question with a named owner.
- Inputs are limited to the fields that endpoint needs, validated locally first.
- Raw responses are reduced to derived fields before storage.
- Stored values carry
checked_atand source information. - No request bodies or raw PII appear in logs or error trackers.
- Retention jobs expire stale or unneeded fields automatically.
- Access is scoped by purpose, and the API key lives in a secrets manager.
- Your privacy notice describes the processing.
If you are enriching large volumes, pair this with careful batching and throttling, and check pricing to size your credit pack.
Frequently asked questions
What is data minimization in an enrichment pipeline?
It means sending only the fields an enrichment step needs and keeping only the derived results you will actually use. For a breach check that is the email address in and a few breach names and dates out, not the whole customer record in both directions.
Should I store the full breach response from Finowl?
Usually not. Store derived fields such as a breached flag, breach names, AddedDate values and a check timestamp, and fetch the full details on demand when someone investigates a specific account.
Is it safe to log email addresses when debugging enrichment jobs?
It is better to avoid it. Log internal record ids or salted hashes, HTTP status and error codes instead, which are enough to debug failures without copying personal data into log storage.
Do I need to mention enrichment in my privacy notice?
If you process names or email addresses, you are processing personal data, so your privacy notice should describe what you check, why and how long you keep it. This is especially important for gender predictions, where people should be able to state or correct their own gender.


