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

Send your key with this call.

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 databases is applied, not a filter on the rows. use searches only the named sources; ignore searches everything except them.

    Sending it without databases is a 400: using an empty allowlist can only match nothing. Sending databases without it is treated as use, 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 with filter to 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, and phone_number. Provenance is nested under source, as source.name and source.date. Rows also carry an opaque id for 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: password and hash are replaced with ******** and the identifier fields are prefix-censored. The field you searched on is never masked, and sensitive_fields is 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 /unlocks to 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 unlocked flag 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_period names. 0 means 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

Send your key with this call.

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

Send your key with this call.

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_terms entries: 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, and phone_number. Provenance is nested under source, as source.name and source.date. Rows also carry an opaque id for 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: password and hash are replaced with ******** and the identifier fields are prefix-censored. The field you searched on is never masked, and sensitive_fields is 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 /unlocks to 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 results is partial.

      • unlocks integer

        Record unlocks spent in the current unlock period.

      • unlocks_limit integer

        Unlock allowance for the current unlock period. 0 means the plan has no unlock mechanism at all, which is every paid tier - and bulk search itself is paid-only, so this is 0 in 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

Send your key with this call.

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