Skip to main content
Credential Check tells your application whether the credential a user just typed appears in a known leak. You call it from your own sign-up, login and password-change flows. Your backend sends what the user typed, and the answer comes back in the same response, so you can refuse a leaked password or ask for a change on the next screen. Each check is one synchronous POST with a JSON body. There is no job to start and nothing to poll. Each call answers one question:
  • Has this password appeared in a known leak, under any login?
  • Has this exact email and password pair leaked together?
  • Has this pair leaked for one website, such as yours?
  • Has this email address appeared in a known leak?
Your users’ addresses can be on any domain, public mail providers included. The checks are not limited to domains your organization owns. Availability: every plan with a license in force. Cost: no metered allowance and no per-check daily cap. Each check made with an API key uses one unit of your monthly API Calls allowance, refunded when the response is not 2xx, and counts toward your organization’s per-second rate limit. Every call appears in your API call usage.

Pick a check for each moment

The checks nest, so each narrower check only adds information when the broader one found something:
  • A pair match is always a password match. The pair check flags only this user’s own leaked pair, not every password someone else leaked.
  • A site match is always a pair match. Run the site check only after the pair check returns found: true.
  • An email match says the address leaked. It does not say the user’s current password did. Use it for a notice, and let the pair check decide whether to prompt a change.
The pair, site and email checks need an email address. If your users sign in with a username, or with an AD-style login such as alice@acme.local, use the password check.

Check a new password

Call credential_check_password_compromised when a user picks a password, before you hash and store it. Send the password as password, exactly as the user typed it. Do not hash or trim it. At sign-up the address is not verified yet, so check the password alone. A pair or email result shown to someone who has not proved they own the address tells them whether that address leaked. At a password change the user is signed in. At a reset, the reset link has proven they control the address. In both cases the pair check with their email and the new password is safe too, if you only want to refuse their own leaked pairs.
  • found: true when at least one leak record holds this password. This is the value your flow acts on.
  • leakCount: the number of leak records that hold this password, under any login. It counts records, not distinct leaks, and stops at 10,000. A value of 10000 means 10,000 or more.
The rules for the value:
  • 1 to 128 characters. An empty string or a longer value is refused with 400. A password longer than 128 characters cannot be checked. Skip the check for it.
  • Whitespace is kept. " hunter2 " and "hunter2" are two different passwords. A single space is a valid password.
  • Matching is case-sensitive. The value is matched as sent, with no wildcards.

Check the pair after a successful login

Login is the one moment your backend holds an existing user’s password in plain text. Your database holds only hashes. That makes login the moment to run the pair check for accounts that already exist.
1

Verify the password as usual

Run your own password verification first. On a failed attempt, call nothing.
2

Run the pair check

Send the email and password the user typed. On found: false, finish the sign-in.
3

Act on a match

Finish the sign-in and prompt a password change. To require the change when the leak names your site, run the site check next.
4

Let the user through if a check cannot answer

On an error or a timeout, finish the sign-in without the prompt. See Let the user through when a check cannot answer.
Call credential_check_credentials_compromised with both fields. A request missing either one is refused with 400.
A record matches only when it holds both values: the address, without regard to case, and the password, case-sensitively. leakCount stops at 10,000, like the password check. found: true means this user’s current password for this address sits in a leak, next to the address.
Run the pair check only after the password verifies. A check on a failed attempt tells whoever is typing whether their guess is a leaked pair. It also still answers, so a credential-stuffing run against your form would use up your organization’s rate limit and push your real checks into 429.

Check a credential for your own site

The password, pair and email checks take no website. The pair check matches the email and password on any site. To ask whether the pair leaked for your site, call credential_check_leak_search with email, password and website in the body.
The response is the leak search envelope: totalElements, page, size and results, in the shape shown under What comes back. Read totalElements. A value greater than 0 means the credential leaked for that site. All three filters must match the same record. A record with no site recorded never matches, so a pair can be found by the pair check and still return 0 here. totalElements stops at 10,000.
  • Send each host your users sign in on, such as acme.com or login.acme.com. website takes one value of up to 255 characters. It is matched, without regard to case and with no wildcards, against the site recorded on each record. Run one search per host.
  • Confirm the value matches before you rely on it. Outside the sign-in path, search once with website alone and read the website values that come back. A value in a form your records do not use answers 0, with no error.
  • A 0 does not clear the user. The pair check already matched, so keep the prompt to change the password.
  • Leave out password to ask whether any credential for this address leaked for your site. Every field you send narrows the match.
  • Send no query string. The default page of 10 records is the smallest one. A size below 10 is refused with 400.
  • Keep results on your server. It carries up to 10 matching records, with the leaked passwords in plain text. Your flow needs only totalElements. Never log or store the response.

Check an email address

Call credential_check_email_compromised for a signed-in user’s verified address, for example on an account or security settings page. Send the address as email.
  • found: true when at least one leak record holds this address.
  • leakCount: the number of matching leak records. It stops at 10,000.
  • firstSeen / lastSeen: when the oldest and the newest matching records were recorded, as YYYY-MM-DD HH:MM:SS. These are recording dates, not breach dates. Both are null when found is false.
The address is matched without regard to case, so Alice@Acme.com and alice@acme.com get the same answer. Surrounding whitespace is trimmed. The field must be a valid email address. A bare domain or a partial address is refused with 400. An address on a reserved domain such as .local, .test or localhost is refused with 422. An address with an IP-address domain is refused with 400 or 422. AD-style logins such as alice@acme.local fall into this case. Skip the check for those addresses. A found address does not mean your user’s password leaked. Read it next to the pair check: if the email is found but the pair is not, the address leaked, but never with this password.

Act on the answer

  • Act on found. Any true is enough to refuse a new password or ask for a change. leakCount tells you how widely a value circulates. It is context, not a threshold.
  • Show results only to the account owner. Run the pair and site checks after a successful login. Show an email result only to the signed-in owner of a verified address. Otherwise your form tells strangers which addresses and passwords are leaked.
  • Store the outcome, not the input. Keep a flag such as “password change required” on the account. Never store the password you checked or the records that came back.
found: false is a complete answer. If the leak index cannot answer from all of its data, the call fails with 502. You never get a smaller count, or found: false, in its place. An error means unknown, never clean.

Wire the checks into your backend

Call Credential Check from your server, inside the request that handles the form. Your API key authorizes calls as your organization. Anyone who reads it can make the same calls, so it never goes to a browser or a mobile app. Your form posts to your backend as it does today, and your backend calls the API. Keep the API key server-side. Read it from your secret store and send it in the X-API-Key header on every check. Authentication covers API keys. A 401 means the key is invalid, expired or revoked. Alert your team, and do not retry with the same key.
1

Send the credential in the JSON body

The checks read password from the request body, never from the query string. Send JSON and build it with a JSON encoder, not string concatenation. Quotes, backslashes and non-ASCII characters in a password then reach the API intact.
2

Set your own timeout

The API waits up to 30 seconds by default for the lookup service before it answers 504. A slow lookup usually ends as a 502 before that. Either way, it is longer than a sign-up or login form should hang. Give the call the wait your form can afford, and treat a timeout as no answer.
3

Fail open

Decide up front what happens when a check gives no answer. At sign-up, login and password change, let the user continue. An outage on the check then never blocks a sign-up or a sign-in.
4

Log the status, never the body

Log the status code of a failed check so you can alert on it. Keep the request body and any leak search response out of your HTTP client logs, error trackers and traces. Both hold credentials.
5

Send the browser only the outcome

The browser needs to know whether to show the warning. It does not need the count or the records.
Never put a password in a URL. A password in the query string is never checked, and URLs are logged far more widely than request bodies.
The helpers below wire the login flow: the pair check, then the site check on a match. Pass in the API key your backend reads from its secret store.
In your login handler, after the password verifies: on "site", require a change before the session continues. On "pair", finish the sign-in and prompt a change. On "none", finish the sign-in. On no answer, finish the sign-in without the prompt. The answer is unknown, not clean. The helpers log the endpoint and the status only. They never log the body, which holds the password.

Let the user through when a check cannot answer

A leak check adds a warning to your flow. It should never be the reason a user cannot sign up, sign in or change a password. When a check fails, finish the flow without the warning and log the failure for your team.

Plan for your volume

Every check is one API call. A sign-in that finds a pair match and then runs the site check makes two. Every call that passes the license check counts toward your organization’s per-second rate limit, failed calls included. That limit is shared with every other call your organization makes. Each of those calls returns X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers. They show your current window, usually one second. Read them to space out bursts. Rate limits covers the limits.
You do not need to check on every sign-in. Record when you last checked an account, and check again on a schedule that fits your traffic. Always check a new password at sign-up, password change and reset.

Read the leak records behind an answer

Support and investigation work sometimes needs the records, not just the yes/no. credential_check_leak_search returns the records a check counted. Read and page the records outside the sign-in path. Send the same values you checked, and totalElements matches the leakCount you got, unless new records were indexed in between.
The records hold plaintext passwords. Show them only to the people who investigate, and keep them out of logs.
1

Run a check

Call the email, password, or pair check. Move on when found is true.
2

Search with the same values

Send the same email, password, or both in the body. Add website to narrow to one site, and dateFrom or dateTo to narrow by date.
3

Page through the records

Raise page until you have read totalElements records.
The filters go in the JSON body. Paging goes in the query string.

Body filters

You need at least one of email, password or website. Dates alone are refused with 400. Every filter you send narrows the result. dateFrom and dateTo each work on their own. Send dates as YYYY-MM-DD. A value longer than 10 characters is refused with 400. An impossible date such as 2024-02-30 is refused with 422.

Query parameters

Records are ordered by createdAt, newest first by default. sort=createdAt,asc returns the oldest first. The API also accepts leakPublishDate and leakDiscoverDate as the field name, but the order still follows createdAt. Only the direction changes it.

What comes back

The envelope:
  • totalElements: the number of matching records. It stops at 10,000, like leakCount.
  • page / size: the page you asked for and its size.
  • results: the records on this page.
There is no page count in the response. Divide totalElements by size and round up. Each record:
  • id: the record’s unique ID.
  • createdAt: when the record was recorded, as YYYY-MM-DD HH:MM:SS. The date filters and the sort use this field.
  • email: the leaked login, as an email address or another login name.
  • password: the leaked password.
  • website: the site the credential was used on.
  • leakId / leakName: the leak the record came from. When the leak was published inside a larger collection, these name the collection.
  • leakPublishDate / leakDiscoverDate: when that leak or collection was published and when it was discovered, as YYYY-MM-DD.
  • fileName: not populated. It is always null.
leakName and the two leak dates come from the leak catalogue. When the catalogue has no entry for the leak, all three are null and leakId is the record’s own leak ID.
Paging stops at record 10,000. A page whose last record would pass 10,000 is refused with 422. At size=100 the last page you can read is 99. At size=10 it is 999. To reach the rest, flip the sort direction or narrow the date range.

Errors

The API validates your request first, then hands it to the lookup service that reads the leak index. A 400 never reaches the leak index. For what a live flow does with each status, see Let the user through when a check cannot answer. Any other error status from the lookup service reaches you unchanged, with its body. Treat it like a 502.

Other credential searches

Credential Check reads the account index, which holds the same parsed login / password / website records the ULP feed searches. You pass the raw value, not a Lucene query. The API escapes it for you, so wildcards, quotes and operators match literally. Start from Find leaked credentials when you need more than a yes/no.

Endpoints