Search
Query the breach corpus by email, username, password, hash, IP, phone, domain, or name. Wildcards on Pro and up, bulk on paid plans.
POST /search
Search the breach corpus
Query the breach corpus by a single term. Use wildcard: true to enable * and ? patterns (Pro and up).
Below the first paid tier the row values come back censored. Every row carries a record_id and an unlocked flag either way, and the envelope carries the unlock balance, so a censored row can be revealed one at a time through POST /unlocks.
What you send.
JSON body, required.
type string required
One of email, username, password, full_name, ip_address, phone_number, hash, domain
term string required
wildcard boolean default false
Pro and up only.
limit integer
offset integer default 0
filter string
How
databasesis applied, not a filter on the rows.usesearches only the named sources;ignoresearches everything except them.Sending it without
databasesis a400: using an empty allowlist can only match nothing. Sendingdatabaseswithout it is treated asuse, since an allowlist is what an unqualified list means.Paid plans only.
One of use, ignore
databases string
Comma-separated source names, for example
website.com,website.org. Paired withfilterto say whether they are the only sources searched or the only ones skipped. Paid plans only.format string
remove_duplicates boolean
Dedupe by email/password/hash.
What comes back.
200 Search results.
success boolean required
data object
results array of object
A row carries the fields the breached source held, so what is populated varies by source. Absent data arrives as an empty string rather than as a missing key.
The data keys are
email,username,password,hash,full_name,ip_address, andphone_number. Provenance is nested undersource, assource.nameandsource.date. Rows also carry an opaqueidfor the record, and further keys may appear without notice, which is why this schema stays open.Below the first paid tier the data fields come back censored:
passwordandhashare replaced with********and the identifier fields are prefix-censored. The field you searched on is never masked, andsensitive_fieldsis stripped on every plan including the top one.Anything the source held outside the known keys arrives under
other_fields. That map is no longer dropped for censored callers: it comes back with the credential-ish and identity-ish subkeys removed (password, hash, salt, secret, token, ssn, dob, card, cvv, pin, security answer, mother's maiden name and the like, including inside nested objects and objects inside lists) and the rest readable. A sensitive subkey is deleted outright, never partially shown.Two named properties are called out below because integrators need them to drive unlocks; the schema stays open for the rest.
record_id string
Stable id for this breach record. Breach rows have no upstream id, so this is a content digest of the row. Present on every plan; pass it to
POST /unlocksto reveal the row.unlocked boolean
Whether this caller has already revealed this record. An unlocked row carries real values even when the response says
censored: true.
Other keys may also be present.
found integer
databases integer
censored boolean
True when sensitive fields were redacted for the caller's plan. It describes the plan, not every row: a row this caller has unlocked carries real values regardless, so read each row's
unlockedflag as well.plan_id string
Starter is a retired legacy id and is not a checkout target.
One of free, starter, professional, premium, team
duplicates_removed integer
response_time_ms integer
unlocks integer
Record unlocks spent in the current unlock period.
unlocks_limit integer
Unlock allowance for the current unlock period, the weekly clock
unlocks_periodnames.0means the plan has no unlock mechanism at all, which is every paid tier, since paid rows are not censored in the first place.
400 The request was malformed or invalid.
success boolean required
error string required
code string
Machine-readable error tag. Omitted whenever it is empty, which is most generic errors, so branch on status too.
One of quota_exceeded, hourly_limit, file_access_limit, plan_upgrade_required, upgrade_required, feature_not_enabled, email_verification_required, monitor_limit, monitor_surface_not_allowed, monitor_first_seen_not_allowed, monitor_webhook_invalid, monitor_webhook_not_allowed, monitor_webhook_key_required
403 Your plan doesn't include this feature. These gates send no `code` at all, and `code` is omitted when empty, so the key is absent from the JSON entirely: branch on the 403 status and not on a code.
The same shape as 400.
429 An allowance was exhausted, or a per-hour cap was hit. Which allowance depends on `metering`: a pooled plan drains one `lookups` pool, while the free tier can run out on one surface with the others still open. The same code covers counters that sit on different clocks, so read `resets_at` and `unlocks_reset_at` from `GET /search/stats` to learn when the allowance you hit comes back.
The same shape as 400.
503 This surface is disabled or unreachable.
The same shape as 400.
POST /search/check
Existence-only check
Returns only the count and source breakdown, useful for ATO or fraud signal at signup time when you do not need the leaked records themselves. The call is metered: it spends one lookup from the same lookups allowance as POST /search. It is cheaper in bandwidth, not in quota.
What you send.
JSON body, required.
type string required
One of email, username, password, full_name, ip_address, phone_number, hash, domain
term string required
What comes back.
200 Yes/no answer plus source count.
success boolean required
data object
found boolean
databases integer
400 The request was malformed or invalid.
success boolean required
error string required
code string
Machine-readable error tag. Omitted whenever it is empty, which is most generic errors, so branch on status too.
One of quota_exceeded, hourly_limit, file_access_limit, plan_upgrade_required, upgrade_required, feature_not_enabled, email_verification_required, monitor_limit, monitor_surface_not_allowed, monitor_first_seen_not_allowed, monitor_webhook_invalid, monitor_webhook_not_allowed, monitor_webhook_key_required
429 An allowance was exhausted, or a per-hour cap was hit. Which allowance depends on `metering`: a pooled plan drains one `lookups` pool, while the free tier can run out on one surface with the others still open. The same code covers counters that sit on different clocks, so read `resets_at` and `unlocks_reset_at` from `GET /search/stats` to learn when the allowance you hit comes back.
The same shape as 400.
503 This surface is disabled or unreachable.
The same shape as 400.
POST /search/bulk
Bulk search up to N terms
Search many terms in a single round-trip. Each term is one lookup: a 100-term Pro call spends 100 of that plan's 250 daily lookups. The per-call ceiling is 100 terms on Pro and 1,000 on Premium and Team, each set well under the plan's daily pool, so one call can never spend a whole day's allowance. Read your own ceiling from bulk_max_terms in GET /search/stats. If the daily allowance runs out mid-batch, the term that ran out comes back with status: quota_exceeded, every term after it is dropped, and the envelope carries quota_exceeded: true alongside the partial results. Per-term timeout is 8s; the whole call has a 90s ceiling. Free returns 403 because bulk search itself is not on that plan.
What you send.
JSON body, required.
type string required
One of email, username, password, full_name, ip_address, phone_number, hash, domain
terms array of string required
Up to
bulk_max_termsentries: 100 on Pro, 1,000 on Premium and Team.wildcard boolean default false
What comes back.
200 One result entry per input term.
success boolean required
data object
total integer
found integer
results array of object
term string
found boolean
count integer
databases integer
results array of object
A row carries the fields the breached source held, so what is populated varies by source. Absent data arrives as an empty string rather than as a missing key.
The data keys are
email,username,password,hash,full_name,ip_address, andphone_number. Provenance is nested undersource, assource.nameandsource.date. Rows also carry an opaqueidfor the record, and further keys may appear without notice, which is why this schema stays open.Below the first paid tier the data fields come back censored:
passwordandhashare replaced with********and the identifier fields are prefix-censored. The field you searched on is never masked, andsensitive_fieldsis stripped on every plan including the top one.Anything the source held outside the known keys arrives under
other_fields. That map is no longer dropped for censored callers: it comes back with the credential-ish and identity-ish subkeys removed (password, hash, salt, secret, token, ssn, dob, card, cvv, pin, security answer, mother's maiden name and the like, including inside nested objects and objects inside lists) and the rest readable. A sensitive subkey is deleted outright, never partially shown.Two named properties are called out below because integrators need them to drive unlocks; the schema stays open for the rest.
record_id string
Stable id for this breach record. Breach rows have no upstream id, so this is a content digest of the row. Present on every plan; pass it to
POST /unlocksto reveal the row.unlocked boolean
Whether this caller has already revealed this record. An unlocked row carries real values even when the response says
censored: true.
Other keys may also be present.
status string
One of success, error, quota_exceeded
error string
censored boolean
plan_id string
Starter is a retired legacy id and is not a checkout target.
One of free, starter, professional, premium, team
quota_exceeded boolean
True when the pooled lookup allowance ran out part-way through the batch, so
resultsis partial.unlocks integer
Record unlocks spent in the current unlock period.
unlocks_limit integer
Unlock allowance for the current unlock period.
0means the plan has no unlock mechanism at all, which is every paid tier - and bulk search itself is paid-only, so this is0in practice.
400 The request was malformed or invalid.
success boolean required
error string required
code string
Machine-readable error tag. Omitted whenever it is empty, which is most generic errors, so branch on status too.
One of quota_exceeded, hourly_limit, file_access_limit, plan_upgrade_required, upgrade_required, feature_not_enabled, email_verification_required, monitor_limit, monitor_surface_not_allowed, monitor_first_seen_not_allowed, monitor_webhook_invalid, monitor_webhook_not_allowed, monitor_webhook_key_required
403 Your plan doesn't include this feature. These gates send no `code` at all, and `code` is omitted when empty, so the key is absent from the JSON entirely: branch on the 403 status and not on a code.
The same shape as 400.
429 An allowance was exhausted, or a per-hour cap was hit. Which allowance depends on `metering`: a pooled plan drains one `lookups` pool, while the free tier can run out on one surface with the others still open. The same code covers counters that sit on different clocks, so read `resets_at` and `unlocks_reset_at` from `GET /search/stats` to learn when the allowance you hit comes back.
The same shape as 400.
GET /stats/compromised
Aggregate corpus metrics
Cached server-side for 1 hour. Aggregate metrics over the corpus.
What comes back.
200 Aggregate metrics.
success boolean required
data object
Opaque payload - counts, timeline, breach catalogue.
Open object, with no fixed keys.
503 This surface is disabled or unreachable.
success boolean required
error string required
code string
Machine-readable error tag. Omitted whenever it is empty, which is most generic errors, so branch on status too.
One of quota_exceeded, hourly_limit, file_access_limit, plan_upgrade_required, upgrade_required, feature_not_enabled, email_verification_required, monitor_limit, monitor_surface_not_allowed, monitor_first_seen_not_allowed, monitor_webhook_invalid, monitor_webhook_not_allowed, monitor_webhook_key_required