Guides
Email breach monitoring for customer accounts: a practical guide
Set up email breach monitoring for customer accounts: schedule checks, diff new breaches by Name and AddedDate, notify users helpfully, and plan credit costs.

Checking an email address for breaches once, at signup, tells you about the past. New breaches are disclosed all the time, and an address that was clean last spring may appear in a fresh dump tomorrow. Email breach monitoring closes that gap by re-checking customer addresses on a schedule and acting only when something new turns up.
This guide walks through a monitoring job built on the Finowl email breach API, from scheduling and diffing to user notifications, password resets and cost planning.
Why monitor instead of checking once
Attackers use leaked credentials for credential stuffing: they take email and password pairs from one breach and try them on other sites. If one of your customers reused a password on a site that was breached, their account with you is at risk even though your systems were never compromised. Our post on credential stuffing and breached passwords explains the attack in detail.
A periodic check lets you spot that exposure and respond before an attacker does. It also gives you a signal to strengthen accounts, for example by prompting for multi-factor authentication.
How the breach check works
The endpoint takes one email address and returns every known breach that includes it.
curl -X POST https://api.finowl.co/v1/breaches/email \
-H "Authorization: Bearer $FINOWL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "alex@example.com" }'A breached address returns breached: true and a breaches array. Each entry includes Name, Title, Domain, BreachDate, AddedDate, PwnCount, Description, DataClasses and a set of flags such as IsVerified, IsFabricated, IsSensitive, IsRetired, IsSpamList, IsMalware and IsStealerLog. A clean address returns { "email": "...", "breached": false, "breaches": [] }. Unverified breaches are included, so check IsVerified before you act. Treat fabricated and spam-list entries with extra caution too.
Designing the monitoring job
A monitoring job has three parts: choose which accounts to check, call the API at a sustainable pace, and compare the results with what you saw last time.
Scheduling
Pick a cadence that matches your risk and budget. Monthly is a reasonable default for most consumer products; weekly makes sense for high-value accounts such as admins, finance users or customers with stored payment methods. You can mix both by assigning a tier to each account.
Spread the work out. Finowl allows 60 requests per minute per account, and a request over the limit returns 429 with code rate_limited and a Retry-After header. Rather than firing the whole list at midnight on the 1st, process a slice of accounts each day so the job stays well under the limit and spreads credit usage evenly. The rate limits documentation covers backoff in more depth.
Diffing by Name and AddedDate
The key to useful monitoring is alerting only on what is new. Store the set of breach Name values you have already seen for each account, along with the latest AddedDate. On each run:
- Fetch the current breaches for the address.
- Compute the breaches whose
Nameis not in the stored set. - Optionally, treat any breach with an
AddedDatelater than your last check as new, which catches cases where your stored set was lost or reset. - Save the updated set and the check timestamp.
BreachDate is when the incident happened; AddedDate is when it was added to breach data. A breach from 2019 might only be added this month, and for monitoring purposes it is still new to you.
A working example
This Node.js script checks one account and returns only new, actionable breaches.
const API = "https://api.finowl.co/v1/breaches/email";
async function checkForNewBreaches(account) {
const res = await fetch(API, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FINOWL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email: account.email }),
});
if (res.status === 429) {
const retryAfter = Number(res.headers.get("Retry-After") || 60);
return { retryAfterSeconds: retryAfter };
}
if (!res.ok) {
const { error } = await res.json();
throw new Error(`Breach check failed: ${error.code}`);
}
const body = await res.json();
const seen = new Set(account.seenBreachNames);
const fresh = body.breaches.filter(
(b) =>
!seen.has(b.Name) &&
b.IsVerified &&
!b.IsFabricated &&
!b.IsSpamList
);
return {
newBreaches: fresh.map((b) => ({
name: b.Name,
title: b.Title,
addedDate: b.AddedDate,
passwordsExposed: b.DataClasses.includes("Passwords"),
})),
allNames: body.breaches.map((b) => b.Name),
checkedAt: new Date().toISOString(),
};
}Save allNames as the new seen set, including unverified breaches, so you do not re-evaluate the same entries next time. Whether you alert on unverified breaches is a policy choice; many teams record them but only notify users about verified ones.
Handling failures safely
A 429 with rate_limited means wait for Retry-After and try again. A 429 with quota_exceeded means you are out of credits, so pause the job rather than retrying. For 5xx or 504 responses, the lookup may have run and may have been charged, so do not retry in a tight loop. Mark the account for the next run instead.
Notifying users helpfully
A breach notification should help the user, not alarm them. Good notifications share a few traits.
Be specific and calm
Name the breached service using Title, say roughly when it happened, and list the kinds of data involved from DataClasses. Make clear that the breach happened on another service, not yours, if that is the case.
Give clear next steps
Tell users what to do: change their password with you if they reused it elsewhere, enable multi-factor authentication, and watch for phishing. Link directly to the relevant settings page. Avoid putting a login link in the email itself, since that trains users to click login links in emails, which phishers exploit.
Respect sensitivity
Some breaches carry IsSensitive: true, meaning the association with the site itself could harm the person. Consider leaving the breach name out of email subjects and previews for these, and show details only after the user signs in.
Forcing password resets responsibly
A forced reset is disruptive, so reserve it for clear risk. Reasonable triggers include:
- A new verified breach whose
DataClassesincludePasswords, combined with signals that the user may have reused the password, such as the current password appearing in breach data. - A new breach plus suspicious activity, such as sign-ins from new devices or locations.
You can test the current password without storing it by checking it at the next login with POST /v1/breaches/password. Finowl hashes the password and looks up only a 5-character hash prefix, and the password is never logged, stored or returned. If breached is true, prompt for a new password then. The same check works in your registration flow, so new accounts never start with a known-breached password.
When you do force a reset, keep the user's session where it is safe to do so, explain why, and make the reset flow short.
Planning costs in credits
Each email breach check costs 1 credit. Every account also gets 100 free credits each month, which reset on the 1st (UTC), do not roll over and are used first. Requests rejected before running, such as invalid emails, are free.
Your monthly usage is roughly the number of accounts checked multiplied by how many times per month you check them.
| Monitored accounts | Cadence | Credits per month |
|---|---|---|
| 5,000 | Monthly | 5,000 |
| 5,000 standard + 500 admins | Monthly + weekly | about 7,000 |
| 40,000 | Monthly | 40,000 |
| 200,000 | Monthly | 200,000 |
Prepaid packs never expire: Starter is 10,000 credits for $29, Pro is 100,000 for $99, Growth is 500,000 for $249 and Business is 1,000,000 for $399 (USD, excluding tax). A 40,000-account monthly job uses about 480,000 credits a year, which fits a Growth pack. See pricing for current details, and how to estimate API credit usage for more worked examples.
To keep costs down, skip closed or long-dormant accounts, validate addresses before sending them, and avoid re-checking the same address twice in one cycle. The email breach monitoring guide in the docs has additional implementation notes, and you can try the endpoint in the Sandbox in your dashboard.
Frequently asked questions
How often should I check customer emails for breaches?
Monthly works for most consumer accounts, with weekly checks for high-value accounts such as admins or users with payment methods. Spread checks across the month to stay within rate limits and keep credit usage steady.
How do I tell if a breach is new for a customer?
Keep the set of breach Name values you have already seen for each account and alert only on names that are not in it. You can also compare AddedDate against your last check time as a backstop.
Should I notify users about unverified breaches?
Unverified breaches are included in the response, so check IsVerified. Many teams store unverified results but only notify users about verified breaches, which reduces noise and false alarms.
Does a breach check cost credits if the email is clean?
Yes. Each completed lookup costs 1 credit whether or not the address is breached. Requests rejected before running, such as an invalid email format, are free.


