Search Data Leaks Dashboard Pricing API Affiliates Contact Blog Crawler Login Register
REST · JSON · HTTPS

API Documentation

Search leaked records and manage Enterprise webhooks programmatically. The API uses standard HTTP methods and returns clean JSON.

Base URLhttps://haveibeenransom.com

Get started in 3 steps

New here? You can make your first call in under a minute. No SDK needed — just curl or any HTTP client.

1

Get your API token

Grab your personal token from your plan dashboard. It looks like a long random string and is yours alone — keep it secret.

2

Add the auth header

Send your token on every request inside the Authorization header, in the form Bearer YOUR_TOKEN.

3

Make your first request

Copy the example on the right, paste in your token, and run it. That's it — you'll get JSON back.

Your first request — copy, paste your token, run

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/metadata/email/[email protected]"

Authentication

Every request (except the public breach counters below) must include your API token. We use the standard Bearer token scheme: put your token in the Authorization header like this:

Authorization: Bearer YOUR_TOKEN

That's the only auth you need — no signatures, no OAuth dance. If the token is missing or wrong you'll get a 401 Unauthorized. Need a token? Pick a plan or email [email protected].

Keep your token private

Anyone with your token can spend your daily quota. Never commit it to public repos or ship it in front-end code. Send requests from your server, not the browser.

You search by field and term. The field tells us what kind of value you're looking for (an email, a domain, a wallet…) and the term is the value itself. Searches are case-insensitive and support partial matching.

You can also search multiple fields at once by separating them with commas — e.g. email,username. The two big datasets accept different fields:

Ransomware leaks & data breaches
nameemailusernamedomainphoneidcountrypassword

Used by metadata, fulldata, fullransom and fullbreach. Great for checking which companies leaked a person's data.

Infostealer logs
emailnamephoneusernameidcountrydomainpasswordwalletssteamidsteamuserteleidteleusertelephonetelelinkvpnftphwid

Used by fullstealer. Includes crypto wallets, gaming accounts, Telegram and machine identifiers.

Field reference

FieldWhat it matchesDatasets
nameFull names and name-related informationall
emailEmail addressesall
usernameUsernames and login identifiersall
domainWebsite domains / leaked email domains — returns every record tied to that domainall
phonePhone numbers and contact infoall
idID / document numbers (DNI, passport…)all
countryCountry (2-letter code, e.g. US, AU)all
passwordPasswords found in breaches / logsall
walletsCryptocurrency wallet addresses (Metamask, Exodus, Bitcoin, Electrum). Electrum entries list every address derived from the cracked seed phrase, space-separated — never the seed itself.stealer
steamidSteam account IDsstealer
steamuserSteam usernamesstealer
teleidTelegram user IDsstealer
teleuserTelegram usernamesstealer
telephoneTelegram phone numbersstealer
telelinkTelegram group linksstealer
vpnVPN domains or IP addressesstealer
ftpFTP domains or IP addressesstealer
hwidHardware / computer IDsstealer

Good to know

  • Some fields map to several underlying columns so you get the widest possible match.
  • Domain searches return all records associated with that domain.
  • Combine fields with commas to widen a search: email,username,name.
  • The email_context value attached to breach records bundles extra personal info (phone, name…) tied to that email.

Endpoint reference

Search endpoints use GET. Resource endpoints such as webhooks also use POST, PATCH, PUT and DELETE as documented below.

Breach counters No auth

Two tiny public endpoints. Get the total number of breaches we track, or the full list of breaches sorted oldest-to-newest.

GET/breaches/total
GET/breaches/full

Example request

curl "https://haveibeenransom.com/breaches/total"

Example response — /breaches/total

{
  "total": 1113
}

Example response — /breaches/full

{
  "breaches": [
    {
      "id": "11111",
      "id_source": "11111",
      "group_name": "ransomgang name",
      "post_title": "Example Company Breached",
      "post_url": "http://example3wh7hnmaiokchk7qoebupfgoik6rhaexample.onion/id=1234",
      "website": "example.com",
      "country": "US",
      "description": "United States",
      "discovered": "2024-12-16 21:07:57.040833",
      "indexed": "2025-07-11T00:33:13.072665",
      "Identities Found": 1872
    }
  ]
}

1 · Metadata search

Which breaches affected this person/domain? Returns lightweight breach metadata (company, group, date, country) without exposing the sensitive records. Results are paginated, 20 per page.

GET/api/metadata/<fields>/<term>
GET/api/metadata/<fields>/<term>/<page>
GET/api/metadata/<fields>/<term>/completeEnterprise

Parameters

ParamMeaningExample
fieldsrequired · what to match: name, phone, email, username, id, country, domain, passwordemail
termrequired · the value to search[email protected]
pageoptional · page number (default 1)2

Example — single field

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/metadata/domain/example.com"

Example — multiple fields

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/metadata/email,username/[email protected]"

Example response

{
  "success": true,
  "results": [
    {
      "Identities Found": 1872,
      "country": "US",
      "description": "United States",
      "discovered": "2024-12-16 21:07:57.040833",
      "group_name": "ransomgang name",
      "id": "11111",
      "id_source": "11111",
      "indexed": "2025-07-11T00:33:13.072665",
      "post_title": "Example Company Breached",
      "post_url": "http://example3wh7hnmaiokchk7qoebupfgoik6rhaexample.onion/id=1234",
      "website": "example.com"
    }
  ],
  "pagination": {
    "current_page": 1,
    "total_pages": 3,
    "total_sources": 45,
    "has_next": true,
    "has_previous": false
  }
}

2 · Full data search

The actual records, not just metadata. Same shape across three sibling endpoints — pick one depending on which dataset you want to search:

EndpointSearches
/api/fulldataEverything combined — ransomware leaks + traditional breaches + ULP/combolists
/api/fullransomRansomware leaks only
/api/fullbreachTraditional data breaches only
GET/api/fulldata/<fields>/<term>
GET/api/fulldata/<fields>/<term>/<id_source>
GET/api/fulldata/<fields>/<term>/completeEnterprise

/api/fullransom/… and /api/fullbreach/… accept the exact same path variants.

Parameters

ParamMeaningExample
fieldsrequired · email, name, phone, username, id, country, domain, passworddomain
termrequired · the value to searchexample.com
search_afteroptional query param · cursor for the next page (default 0)?search_after=253941
id_sourceoptional · restrict results to one breach source11111
sortoptional · date_desc (newest first) or date_asc. Professional plan or above. Changes the cursor format — see below?sort=date_desc

Paid plans only

Full data endpoints require a paid data plan. Requests from a Basic token get a 403 asking you to upgrade.

Example — single field

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/fulldata/domain/example.com"

Example — next page (cursor pagination)

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/fulldata/email/[email protected]?search_after=253941"

Example response

{
  "success": true,
  "email": "[email protected]",
  "data": [
    {
      "email": "[email protected]",
      "email_context": "phone, name, etc",
      "From archive": "This domain has been in PDF archive example.pdf",
      "source_metadata": {
        "country": "US",
        "description": "Lorem ipsum......",
        "group_name": "play",
        "company_affected": "Company XYZ",
        "post_url": "http://example3wh7hnmaiokchk7qoebupfgoik6rhaexample.onion/id=1234",
        "website": "example.com"
      }
    }
  ],
  "has_next_page": true,
  "search_after": 253941,
  "total_hits": 325478
}

To page through results, take search_after from the response and pass it back as ?search_after=.... When has_next_page is false, you've reached the end.

Sorting by date — and what it does to your cursor

Add ?sort=date_desc (newest first) or ?sort=date_asc to order results by publication date. Available on Professional plans and above; other plans get a 403 with error_code: "sort_requiere_plan". It works on fulldata, fullransom, fullbreach and fullstealer.

Read this before you start sending it. Without sort, nothing changes: search_after is the same integer cursor it has always been. With sort, search_after becomes an opaque string such as "A:eyJpIjozfQ". Do not parse it, do not cast it to an integer, do not assume it keeps growing. Send it back exactly as you received it, together with the same sort value — the cursor from a sorted search means nothing without it. If your client currently reads the cursor as a number, it will break the day you add sort; leave sort off and nothing about your integration changes.

Sorted responses carry two extra fields: sort (echoing what you asked for) and sort_applied. On very broad infostealer searches you may also get sort_error: "sort_no_convergente", which means the scan hit its work limit for that page — the results you got are still correctly ordered, and you can keep paging with the cursor. /complete exports ignore sort and say so with sort_ignored_on_complete.

3 · Full stealer search Infostealer logs

Stolen credentials, crypto wallets, gaming accounts, Telegram and more — straight from infected machines. This is the richest dataset, so its response can take three different shapes. Read the top-level "mode" first.

GET/api/fullstealer/<fields>/<term>
GET/api/fullstealer/<fields>/<term>?search_after=X
GET/api/fullstealer/<fields>/<term>/completeEnterprise

Parameters

ParamMeaningExample
fieldsrequired · any infostealer field (email, username, wallets, steamid, teleuser, vpn, country…)email
termrequired · the value to search (country must be a 2-letter code)[email protected]
search_afteroptional query param · cursor for the next page (default 0)?search_after=2587
sortoptional · date_desc / date_asc by infostealer log date. Professional plan or above. Changes the cursor format — see the note under Full data?sort=date_desc

First, how infostealer data is organized

When a machine is infected, the stealer dumps everything into 5 buckets (indexes):

  • Credential → saved logins (URL / user / password)
  • Cookies → browser cookies / sessions
  • Info → system info (Country, OS, HWID, log date…)
  • plus → extras: crypto wallets, VPN, FTP, Steam, Telegram…
  • Filelist → list of files grabbed from the machine

Every document shares the same Filename — a unique ID for the infected machine, e.g. HWID E32F8AFB9CF347BA22FCCF354C9E9B5C_analysis14951. Think of Filename as the "victim machine ID" that glues all the pieces together.

The response has 3 shapes — always check "mode"

The field you search by decides the shape. Read the top-level "mode" value and parse accordingly:

modeWhen you search by…What you get
fragmentsemail, username, phone, id, domain, passwordA flat list in "matches", one item per credential: {URL, USER, PASS, Filename}.
documentswallets, steamid, steamuser, teleid, teleuser, telephone, telelink, vpn, ftp, hwidRaw documents in "data", each as {_index, _source}.
stealerscountryResults grouped per infected machine in "data" — every document from all 5 indexes bundled together.

Example — by email (mode: fragments)

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/fullstealer/email/[email protected]"

Example — by country (mode: stealers)

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/fullstealer/country/AU"

Response A — mode: "fragments"

When you search by email, username, phone, id, domain or password. Each item in matches is one matching login; Filename tells you which machine it came from.

{
  "mode": "fragments",
  "matches": [
    {
      "URL": "https://login.example.com",
      "USER": "[email protected]",
      "PASS": "leaked_password_123",
      "Filename": "HWID E32F8AFB9CF347BA22FCCF354C9E9B5C_analysis14951"
    }
  ],
  "count": 1,
  "total_docs": 14356,
  "search_term": "example.com",
  "fields": ["domain"],
  "search_after": 2587,
  "has_next_page": true
}

Response B — mode: "documents"

When you search by wallets, steamid, steamuser, Telegram fields, vpn, ftp or hwid. Each entry in data is a raw document; the real content lives in _source, and _index tells you which bucket it came from.

{
  "mode": "documents",
  "data": [
    {
      "_id": "kJ3l9ZcBxq",
      "_index": "logs_plus",
      "_score": null,
      "_source": {
        "Filename": "HWID E32F8AFB9CF347BA22FCCF354C9E9B5C_analysis14951",
        "HWID": "E32F8AFB9CF347BA22FCCF354C9E9B5C",
        "Country": "AU",
        "wallets": "Electrum:bc1qxy2k9... bc1qz93mn... bc1q7cyq3..."
      }
    }
  ],
  "count": 1,
  "total_hits": 320,
  "search_term": "76561198012345678",
  "fields": ["steamid"],
  "has_next_page": false
}

Response C — mode: "stealers" (country search)

Only when you search by country. data is a list of infected machines. Each machine has its filename, a count of its documents, and a documents array bundling everything found for that machine across the 5 indexes.

{
  "mode": "stealers",
  "data": [
    {
      "filename": "HWID E32F8AFB9CF347BA22FCCF354C9E9B5C_analysis14951",
      "count": 4,
      "documents": [
        {
          "_index": "logs_info",
          "_source": {
            "Filename": "HWID E32F8AFB...analysis14951",
            "Country": "AU",
            "HWID": "E32F8AFB9CF347BA22FCCF354C9E9B5C",
            "LogDate": "2024-11-02"
          }
        },
        {
          "_index": "logs_credential",
          "_source": {
            "Filename": "HWID E32F8AFB...analysis14951",
            "Credentials": [
              {
                "URL": "https://mail.example.com",
                "USER": "[email protected]",
                "PASS": "hunter2"
              }
            ]
          }
        },
        {
          "_index": "logs_plus",
          "_source": {
            "Filename": "HWID E32F8AFB...analysis14951",
            "wallets": "Metamask:0x9aF3...",
            "VPN info": "nordvpn.com"
          }
        },
        {
          "_index": "logs_filelist",
          "_source": {
            "Filename": "HWID E32F8AFB...analysis14951",
            "Files": ["C:\\Users\\victim\\Desktop\\passwords.txt"]
          }
        }
      ]
    }
  ],
  "count": 1,
  "total_hits": 5821,
  "search_term": "AU",
  "fields": ["country"],
  "search_after": 1024,
  "has_next_page": true
}

How to read it

  • count (top level) → machines returned on this page.
  • total_hits → total country matches across all pages.
  • data[].count → how many documents that single machine has.
  • Use search_after + has_next_page to page through more machines.

4 · Leak analytics

Profile a single breach. Give it a breach's id_source and get aggregated analytics: corporate vs personal email split, how many identity documents, a file-type breakdown, and the breach's metadata.

GET/api/fullmetadata/<id_source>

Parameters

ParamMeaningExample
id_sourcerequired · the breach source ID (from a metadata or breaches result)11111

Example request

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/fullmetadata/11111"

Example response

{
  "id_source": "11111",
  "leak_type": "ransomware_leak",
  "total_records": 1872,
  "breach_metadata": {
    "post_title": "Example Company Breached",
    "group_name": "play",
    "country": "US",
    "discovered": "2024-12-16 21:07:57.040833"
  },
  "email_origin": {
    "total_emails": 1500,
    "records_with_email": 1450,
    "internal_corporate": { "count": 900, "percentage": 60.0 },
    "external_personal": { "count": 600, "percentage": 40.0 }
  },
  "identity_documents": {
    "count": 120,
    "percentage_of_records": 6.41
  },
  "file_types": {
    "distinct_types_present": 4,
    "breakdown": {
      "pdf": 150,
      "csv": 80,
      "xlsx": 30,
      "databases": 200
    }
  }
}

5 · Domain overview Enterprise

One call, full picture of a domain's exposure. Runs infostealer (clients & employees), breaches and ransomware lookups in parallel and returns the totals plus top URLs and users. Non-Enterprise tokens receive a safe demo response with "is_demo": true.

GET/api/domain_overview/<domain>

Parameters

ParamMeaningExample
domainrequired · the domain to profile (use example.* to match all TLDs)example.com

Example request

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/domain_overview/example.com"

Example response (trimmed)

{
  "domain": "example.com",
  "exposures": {
    "infostealer": {
      "clients": {
        "total_exposures": 1820,
        "unique_urls": 45,
        "urls": [{ "url": "https://login.example.com", "count": 320 }],
        "users": [{ "user": "[email protected]", "count": 12 }],
        "status": "success"
      },
      "employees": {
        "total_exposures": 230,
        "unique_urls": 8,
        "urls": [],
        "users": [{ "user": "[email protected]", "count": 3 }],
        "status": "success"
      },
      "total": 2050
    },
    "breaches": {
      "count": 540,
      "unique_sources": 6,
      "sources": [
        { "source_id": "abc123", "count": 320, "metadata": { "name": "Acme DB" } }
      ],
      "status": "success"
    },
    "ransomware": {
      "count": 1872,
      "unique_sources": 3,
      "sources": [
        { "source_id": "11111", "count": 1872, "metadata": { "name": "play" } }
      ],
      "status": "success"
    }
  },
  "total_exposures": 4462,
  "execution_time_seconds": 1.234,
  "timestamp": 1735000000.0
}

5b · Domain exposure report Enterprise

A grade, a spoofing verdict and a trust level for a domain. Counts the distinct incidents that affect users of the domain (infostealer infections, ransomware leaks and breaches), grades the total from A to E, checks SPF, DMARC and DKIM to tell whether the domain's email can be spoofed, lists exposed services found through passive DNS, and combines everything into a 0–100 trust level. No connection is made to the domain's own hosts. Non-Enterprise tokens receive a demo response with "is_demo": true.

GET/api/domain_report/<domain>

Parameters

ParamMeaningExample
domainrequired · a plain domain name (no wildcards, URLs or emails; otherwise 400)example.com

How it is scored

FieldMeaning
breaches_totalstealers + ransomware + breaches: infected devices with a credential for a user of the domain, distinct ransomware leaks and distinct breaches containing emails of the domain. Raw record counts are in records.
gradeA = 0 · B = 1–10 · C = 11–25 · D = 26–50 · E = more than 50
email_spoofing.protectedtrue when there is exactly one valid SPF record (at most 10 DNS lookups, RFC 7208 §4.6.4, following include and redirect=) ending in -all or ~all AND a DMARC record with p=quarantine or p=reject applied to 100% of mail. A subdomain without its own _dmarc record uses the parent domain's sp=. Multiple DMARC records, or a record where v=DMARC1 is not the first tag, are invalid (dmarc.status: "invalid"). DKIM is checked on common selectors and reported, but is not used to decide (selectors cannot be listed through DNS). spoofable is always the opposite of protected; it is kept for compatibility.
email_spoofing.levelstrong = -all and p=reject; weak = protected with ~all or p=quarantine; none = not protected. sends_mail (true / false / null) is deduced from the senders the SPF record authorises and, without them, from a real MX. recommendation gives the exact records to publish to reach strong (spf, dmarc, mx, plus notes); for a domain that sends no mail it is v=spf1 -all, v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s and a null MX 0 . (RFC 7505). null when nothing needs to change. warnings lists issues that do not change the verdict: an invalid MX, +all, or sp=none / np=none on a protected domain. spf.lookups, spf.errors, dmarc.rua and dmarc.np give the details.
email_spoofing.dkimA selector counts as found only if its p= key is not empty and is valid base64 of a plausible length. An empty p= is a revoked key (RFC 6376 §3.6.1) and is listed in selectors_revoked; a malformed key goes to selectors_invalid. A random selector is queried first: if it answers, the domain has a wildcard *._domainkey record, so wildcard is true, wildcard_key says whether that key is revoked, valid or invalid, and no selectors are listed. status: found (a valid key), revoked (the domain only publishes revoked keys: it declares that it does not sign with DKIM), not_determined (does not prove DKIM is absent) or unknown.
email_spoofing.mx_validfalse when the domain publishes an MX that cannot work: an MX that points to an IP address (RFC 5321 requires a host name), an MX host that does not resolve, or a null MX mixed with other records; the problem also appears in warnings. No MX and a lone null MX (".", RFC 7505: the domain does not receive email) are valid. null if DNS did not answer. Details in email_spoofing.mx. The MX check does not change protected.
exposed_servicesPassive DNS only: MX/NS plus common service names (VPN, webmail, admin, devops, staging, FTP…) that resolve. The domain itself and www are excluded; wildcard DNS zones are detected and skipped.
trust_level0.6 × breach score (A=100, B=75, C=50, D=25, E=0) + 0.4 × email score (level strong 100, weak 60, none 0) − 2 per sensitive exposed service (max 10), from 0 to 100. Grade A with strong protection is always 100; grade E and spoofable is always 0. If email protection could not be checked (DNS error) it is left out and does not penalise: breach score − services penalty, with trust_detail.email_score: null.

Example request

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/domain_report/example.com"

Example response (trimmed)

{
  "domain": "example.com",
  "breaches_total": 14,
  "breakdown": { "stealers": 3, "ransomware": 1, "breaches": 10 },
  "records": { "stealer_credentials": 5, "ransomware_records": 120, "breach_records": 2300 },
  "grade": "C",
  "email_spoofing": {
    "status": "ok",
    "protected": false,
    "spoofable": true,
    "level": "none",
    "sends_mail": true,
    "mx_valid": true,
    "reasons": ["DMARC policy none"],
    "warnings": [],
    "recommendation": {
      "spf": null,
      "dmarc": "v=DMARC1; p=reject; rua=mailto:[email protected]",
      "mx": null,
      "notes": ["before p=reject, check with DMARC reports (rua=) that every legitimate sender passes SPF or DKIM"]
    },
    "spf": { "present": true, "all_qualifier": "-all", "enforcing": true, "lookups": 3, "errors": [] },
    "dmarc": { "status": "ok", "present": true, "policy": "none", "pct": 100, "rua": ["mailto:[email protected]"], "np": null, "enforcing": false },
    "dkim": { "status": "found", "wildcard": false, "selectors_checked": 22, "selectors_found": ["selector1"], "selectors_revoked": [] },
    "mx": { "status": "ok", "present": true, "records": ["mx1.example.com"], "null_mx": false, "valid": true }
  },
  "exposed_services": {
    "status": "ok",
    "wildcard_dns": false,
    "mx": ["mx1.example.com"],
    "hosts": [{ "host": "vpn.example.com", "category": "remote_access", "sensitive": true }],
    "sensitive_count": 1
  },
  "trust_level": 28,
  "trust_detail": { "breach_score": 50, "email_score": 0, "base": 30.0, "services_penalty": 2 },
  "execution_time_seconds": 0.94
}

If DNS does not answer (timeout or SERVFAIL), email_spoofing.status is "error" with protected, spoofable and level set to null and reasons: ["DNS error"]; trust_level is then computed without the email score. exposed_services.status is "unknown" with a reason. A domain that does not exist (NXDOMAIN) returns 404 with {"domain": "...", "domain_exists": false, "error": "domain does not exist"} and is not rated. If the breach data source is unavailable the call returns 502.

5c · Domain exposure report · medium Enterprise

Who is exposed, where, and how well the domain is protected. Counts the distinct exposed users of a domain and splits them into employees and customers. It also lists the exposed services and the 10 URLs of the domain with the most exposed credentials, detects the email provider, gives an OK / NOT OK verdict for SPF, DKIM and DMARC, checks whether the domain's SSL certificate is valid, and returns the 3 breaches with the most users of the domain, sorted by publication date. No credentials or passwords are returned. Non-Enterprise tokens receive a demo response with "is_demo": true.

GET/api/domain_report_medium/<domain>

Parameters

ParamMeaningExample
domainrequired · a plain domain name (no wildcards, URLs or emails; otherwise 400)example.com

What each field means

FieldMeaning
users_totalDistinct exposed users of the domain: employees.count + customers.count.
employeesUsers whose email belongs to the domain: infostealer credentials with an @domain user, plus emails of the domain in breaches and ransomware leaks. pct is the share of users_total.
customersUsers with an infostealer credential for a URL of the domain or one of its subdomains, whose username is not an email of the domain. A user is counted once: an employee is never also a customer.
services_totalDistinct subdomains (excluding the domain itself and www) that are the target of exposed credentials. services lists up to 50.
top_urlsThe 10 URLs of the domain with the most exposed accounts. URLs are returned without username, password, query string or fragment.
email_providerDeduced from the MX records (Google Workspace, Microsoft 365, Proton Mail, Zoho, IONOS…, or a security gateway such as Proofpoint or Mimecast). self-hosted if the MX is in the domain itself, other if it is not recognised (see mx), none if there is no MX. mx_valid is false when the domain publishes an MX that cannot work (no MX and a lone null MX are valid); an MX that points to an IP address or to a host that does not resolve adds a warning.
email_spoofingspf OK = exactly one valid SPF record (at most 10 DNS lookups) ending in -all or ~all. dmarc OK = p=quarantine or p=reject applied to 100% of mail. dkim OK = a valid public key (non-empty, valid base64 p=) on at least one common selector or in a wildcard record. A wildcard *._domainkey record that answers every selector with an empty (revoked) key, or only revoked keys, is NOT OK with detail.dkim.status: "revoked"; NOT OK does not prove DKIM is absent, because selectors cannot be listed through DNS. UNKNOWN = DNS did not answer. level, sends_mail, warnings and recommendation work as in the basic report. detail holds the records and policies found.
sslOne TLS handshake to <domain>:443 (to www.<domain> only if the domain does not accept connections). It validates the chain, the hostname and the validity dates. valid: true includes the issuer, expiry and days_left. false includes a reason such as certificate has expired. null with "status": "UNKNOWN" means no HTTPS service was reachable. No other ports are touched.
top_breachesThe 3 breaches or ransomware leaks with the most distinct users of the domain (users), sorted by published_at with the newest first and unknown dates last. Infostealer logs are not breaches and are not included.
coverageapproximate: true on very large domains: more than 10,000 infostealer logs match, or the employee count was estimated from a sample. The reason is in notes.

Example request

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/domain_report_medium/example.com"

Example response (trimmed)

{
  "domain": "example.com",
  "report": "medium",
  "users_total": 120,
  "employees": { "count": 80, "pct": 66.7, "from_infostealers": 12, "from_breaches_and_ransomware": 70 },
  "customers": { "count": 40, "pct": 33.3 },
  "services_total": 3,
  "services": ["intranet.example.com", "portal.example.com", "vpn.example.com"],
  "top_urls": [
    { "url": "https://portal.example.com/login", "credentials": 25 },
    { "url": "https://example.com/", "credentials": 11 }
  ],
  "email_provider": { "provider": "Microsoft 365", "mx": ["example-com.mail.protection.outlook.com"], "mx_valid": true },
  "email_spoofing": {
    "spf": "OK", "dkim": "OK", "dmarc": "NOT OK",
    "level": "none", "sends_mail": true, "warnings": [],
    "recommendation": { "spf": null, "dmarc": "v=DMARC1; p=reject", "mx": null, "notes": ["..."] },
    "detail": { "dmarc": { "policy": "none", "reason": "DMARC policy none" } }
  },
  "ssl": { "status": "OK", "valid": true, "host": "example.com", "issuer": "Let's Encrypt",
           "expires_at": "2026-12-31T23:59:59Z", "days_left": 86, "hostname_match": true },
  "top_breaches": [
    { "name": "Example breach A", "type": "breach", "users": 40, "published_at": "2026-05-11" },
    { "name": "Example ransomware leak", "type": "ransomware", "users": 22, "published_at": "2025-11-24" },
    { "name": "Example breach B", "type": "breach", "users": 31, "published_at": "2024-03-11" }
  ],
  "coverage": { "approximate": false, "notes": [] },
  "execution_time_seconds": 6.2
}

A domain with no exposure returns zeros and empty lists. A domain that does not exist (NXDOMAIN) returns 404 with {"domain": "...", "domain_exists": false, "error": "domain does not exist"}. If the breach data source is unavailable the call returns 502. Large domains can take up to about 30 seconds.

6 · Document images Enterprise

The actual image files. Every breach record shows a Found in Archive filename — the leaked document it was extracted from. Those documents are catalogued by content hash, so you can look one up and fetch the raw image. Enterprise only: any other plan gets a 403.

GET/api/images/search?name=<filename>&id_source=<id>&hash=<sha256>Enterprise
GET/api/images/by-leak/<id_source>Enterprise
GET/img/<sha256>Enterprise

Parameters — /api/images/search

ParamWhat it doesExample
nameFilename to look up (the Found in Archive value). Case-insensitive.@tayyub147 (721).jpg
id_sourceRestrict to one leak (the record's Source ID). Recommended — filenames repeat across leaks.16547
hashExact sha256, if you already have it.9f3a…e21
size / fromPagination. Default size 20, max 100.20

How to get an image (2 steps)

Search resolves the file to its content hash; then you fetch the bytes. Each result already carries a ready-to-use url.

# 1) find the image for a document, scoped to its leak
curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/images/search?name=@tayyub147%20(721).jpg&id_source=16547"

# 2) fetch the raw image (the "url" from step 1)
curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/img/9f3a…e21" --output document.jpg

Response — /api/images/search

{
  "total": 1,
  "count": 1,
  "results": [
    {
      "sha256": "9f3a…e21",
      "ext": "jpg",
      "mime": "image/jpeg",
      "bytes": 543210,
      "id_sources": ["16547"],
      "filenames": ["@tayyub147 (721).jpg"],
      "url": "/img/9f3a…e21"
    }
  ]
}

/api/images/by-leak/<id_source> returns the same result shape for every image of a leak (paginated with size/from).

Enterprise only. All three endpoints require an Enterprise token (or an Enterprise dashboard session). Other plans get 403 plan_required. Images are served with X-Robots-Tag: noindex and a private cache, and are never linked from public pages.

7 · Batch lookups POST

Up to 100 terms in one request. Looking up 40 machines one at a time is 40 round-trips and 40 quota requests. Send them as a batch instead: the whole batch resolves in a single Elasticsearch call and comes back grouped by term. Measured against production: 10 terms in 149 ms, versus 1.9 s for the same 10 one at a time.

POST/api/bulk/fullstealer/filename
POST/api/bulk/fullstealer/hwid

Limits

RuleValue
Terms per batch10 — 100 on Enterprise
Quota costOne request per term, not per call. A 10-term batch spends 10 of your daily requests.
PlansSame gating as fullstealer: basic gets 403.
Fieldsfilename and hwid. Anything else returns 400 with the supported list.
Result sizeEvery batch returns the complete log for each term — there is no paging and no search_after.

Request

curl -X POST "https://haveibeenransom.com/api/bulk/fullstealer/filename" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"terms":["HWID AAA..._analysis1","HWID BBB..._analysis2"]}'

Response

Each results[term].data holds the same document shape the single-term endpoint returns, so existing parsing code works unchanged. A term that matched nothing comes back with success: true and an empty data — it is not an error. A term whose sub-search failed lands in errors and never silently disappears.

{
  "results": {
    "HWID AAA..._analysis1": {
      "success": true,
      "mode": "documents",
      "total_hits": 5,
      "data": [ { "_id": "...", "_index": "logs_credential-000002", "_source": { } } ]
    },
    "HWID BBB..._analysis2": { "success": true, "mode": "documents", "total_hits": 0, "data": [] }
  },
  "errors": {},
  "meta": {
    "terms_requested": 2,
    "terms_found": 1,
    "elapsed_ms": 149,
    "quota_consumed": 2,
    "cached": 0,
    "daily_limit": 20000,
    "remaining": 19998
  }
}

When the batch does not fit your quota the whole call is rejected with 429 and nothing is charged. The body carries terms_that_fit so you can resend a smaller batch instead of guessing.

8 · Page size Enterprise

Fewer, bigger pages. The paginated endpoints return a fixed number of rows per call. On Enterprise tokens you can raise that with page_size and cut a sweep to a fraction of the calls: 600 records goes from ~20 requests to 3. Enterprise only — on any other plan the parameter is ignored and the response is byte-for-byte what it has always been.

GET/api/metadata/<fields>/<term>?page_size=<n>Enterprise
GET/api/fulldata/<fields>/<term>?page_size=<n>Enterprise
GET/api/fullstealer/<fields>/<term>?page_size=<n>Enterprise

Rules

RuleValue
Aliassize works too.
Maximum200
Defaultsmetadata 20 · fulldata 30 · fullstealer 20
Out of rangeClamped to the maximum — not an error. The response always reports the page_size actually applied.
Not a number400. A malformed value is never ignored silently.
Quota costOne request, whatever the page size.
Complete modeUnaffected — /complete already returns everything.
# 100 rows per page instead of 30
curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/fulldata/domain/acme.com?page_size=100"

# metadata recalculates total_pages / has_next / next_page for the size you asked for
curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/metadata/domain/acme.com/2?page_size=100"

Cursors are tied to the page size. On metadata a page number means different rows at a different page_size. If you are walking a result set, keep page_size constant for the whole walk — changing it mid-way skips or repeats rows. search_after cursors are unaffected.

Password toolkit

A password-centric family of endpoints on top of the breach, infostealer and ULP data. Unlike the search endpoints above, these are parameterised with the query string — the base is always /api/pass.

Plans & masking. Same gating as fulldata: basic gets 403. Two levels need Professional or above — the unfiltered stream (/api/pass with no filter) and /api/pass/stats. Without the clear add-on, real leaked passwords come back masked (first 3 chars + a marker); raw internal fields (ipas, pas) are never returned. The listing endpoints return data as a plain list of password strings; use include=full for the surrounding credential. On /similar and /check the query password is logged hashed, never in clear.

Password stream & lookups

The full password dataset, or every password tied to a domain/email. Paginated 50 at a time with an opaque search_after cursor (send it back verbatim for the next page).

GET/api/pass?limit=50Professional+
GET/api/pass?domain=<domain>
GET/api/pass?email=<email>
GET/api/pass?domain=<domain>&include=full

Parameters

ParamMeaningExample
domain / emailfilter by a credential's domain or a specific email. No filter = the whole dataset (Professional+).acme.com
includeoptional · full returns the whole infostealer machine log instead of loose passwordsfull
limitoptional · results per page (default 50, max 50)50
search_afteroptional · opaque cursor from the previous pageeyJwaGFzZSI6...

Example — passwords for a domain

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/pass?domain=acme.com&limit=50"

Example response — only passwords

{
  "level": "domain",
  "count": 3,
  "source_breakdown": { "ulp": 3, "breach": 0, "stealer": 0 },
  "search_term": "acme.com",
  "has_next_page": true,
  "search_after": "eyJwaGFzZSI6ImNvbWJpIiwic2EiOjExOTMxMTh9",
  "data": ["1397HASSAn", "anin1108", "p@ssw@rd"]
}

Example response — include=full (whole machine log)

{
  "level": "full",
  "count": 1,
  "data": [
    {
      "filename": "HWID E32F8AFB..._analysis14951",
      "count": 68,
      "log_date": "2024-11-02",
      "stealer_family": "Redline",
      "credentials": [
        { "URL": "acme.com", "USER": "[email protected]", "PASS": "1397HASSAn" }
      ]
    }
  ]
}

Similar-password generator passgen

Give a base password, get a ranked wordlist of look-alikes (case, roots, reverse, leet, appended years/numbers/symbols, and characters before the first letter). Add check=1 to keep only the ones that actually leaked.

GET/api/pass/similar?q=<password>
GET/api/pass/similar?q=<password>&check=1

Parameters

ParamMeaningExample
qrequired · base password (min 4 chars). Logged hashed.Kropek1991T
limitoptional · candidates to generate (default 400 ≈ 200 core + 200 prefixed, max 5000)400
checkoptional · 1 → return only candidates that exist in the data (masked)1

Example — generate a wordlist

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/pass/similar?q=Kropek1991T&limit=400"

Example response

{
  "level": "similar",
  "mode": "generate",
  "query": "sha256:313184e33a7f",
  "count": 400,
  "data": [
    "Kropek1991T", "kropek1991t", "KROPEK1991T", "Kropek", "Kropek1991",
    "T1991keporK", "Kr0pek1991T", "Kropek!", "Kropek123", "Kropek2024",
    "1Kropek1991T", "!Kropek1991T", "123Kropek1991T", "2024Kropek1991T"
  ]
}

With &check=1 the response becomes "mode": "found" and data holds only the candidates that really leaked (masked according to your plan).

Analytics & check

An aggregate password report for a domain, and a pwned-style existence counter for a single password.

GET/api/pass/stats?domain=<domain>Professional+
GET/api/pass/check?q=<password>

Example — domain analytics

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/pass/stats?domain=acme.com"
{
  "level": "stats",
  "search_term": "acme.com",
  "total_credentials": 128,
  "unique_passwords": 96,
  "reuse_ratio": 0.25,
  "top_passwords": [ { "password": "Acme2024!", "count": 7 } ],
  "length_distribution": { "lt_8": 12, "8_11": 74, "12_15": 33, "gte_16": 9 },
  "pattern_flags": { "has_year": 21, "only_digits": 8, "common_suffixes": { "!": 14 } },
  "source_breakdown": { "ulp": 90, "stealer": 38 }
}

Example — check a password

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/pass/check?q=Kropek1991T"
{
  "level": "check",
  "query": "sha256:313184e33a7f",
  "found": true,
  "count_stealer": 7,
  "count_ulp": 12,
  "count_breach": 0,
  "total": 19
}

Enterprise webhooks

Enterprise API tokens can register HTTPS callbacks and receive signed event deliveries. Send the same Authorization: Bearer YOUR_TOKEN header used by the search API. A missing or invalid token returns 401; a valid non-Enterprise token returns 403 without consuming API quota.

Enterprise only

Webhook access and delivery require an active Enterprise entitlement. If the account is downgraded or deactivated, future deliveries stop automatically.

GET/api/webhooks
POST/api/webhooks
GET/api/webhooks/<id>
PATCH/api/webhooks/<id>
DELETE/api/webhooks/<id>
POST/api/webhooks/<id>/test
POST/api/webhooks/<id>/rotate-secret
POST/api/webhooks/<id>/scan

The callback URL must use HTTPS, have public DNS and resolve only to globally routable addresses. Internal, loopback, link-local, reserved, metadata and redirect destinations are rejected. Each account can see and change only its own webhook IDs.

Create a webhook

curl -X POST "https://haveibeenransom.com/api/webhooks" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.example.com/hibr","events":["new_breach"],"description":"Security intake"}'
{
  "success": true,
  "webhook": {
    "id": "wh_example",
    "url": "https://hooks.example.com/hibr",
    "events": ["new_breach"],
    "active": true,
    "secret": "SHOWN_ONCE"
  }
}

Store the secret when it is created or rotated; later responses expose only a masked hint. Update URL, events, active state, description, watch_emails or watch_domains with PATCH or PUT. DELETE unregisters the webhook. The test route sends one synthetic signed delivery. The scan route checks the configured watchlist; append ?deliver=0 for a dry run.

Signed delivery

{
  "event_type": "new_breach",
  "id_source": "example-id",
  "occurred_at": "2026-10-08T12:00:00Z",
  "delta": {}
}

Verify X-DarkEye-Signature as the lowercase HMAC-SHA256 hex digest of the exact raw request body using your webhook secret. Return any 2xx status to acknowledge the delivery. Failed deliveries are retried with backoff; redirects are never followed.

Management examples

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/webhooks"

curl -X PATCH "https://haveibeenransom.com/api/webhooks/wh_example" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"active":false}'

curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/webhooks/wh_example/test"

curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/webhooks/wh_example/rotate-secret"

curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/webhooks/wh_example/scan?deliver=0"

curl -X DELETE -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/webhooks/wh_example"

Management errors use JSON with success: false: 401 for authentication, 403 when Enterprise is required, 404 for an unknown or foreign webhook ID, and 409 for limits or an overlapping scan.

Complete mode

Add /complete to the end of a metadata, fulldata, fullransom, fullbreach or fullstealer request to get every result at once, with no pagination.

Enterprise only

Because /complete can return enormous datasets, it's reserved for Enterprise license holders. Other plans get a 403 telling them to upgrade. Everyone else should page normally with page (metadata) or search_after (fulldata / fullstealer).

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://haveibeenransom.com/api/fulldata/email/[email protected]/complete"

Rate limits

Each plan includes a daily API quota (requests per day). Every response carries rate-limit headers so you can track your usage. Go over the limit and you'll get a 429 Too Many Requests until the next reset.

Basic
100requests / day
Professional
2,000requests / day
Cybersecurity Agency
20,000requests / day
Enterprise / Gov
∞unlimited
PlanAPI requests / dayComplete mode
Basic100—
Professional2,000—
Cybersecurity Agency20,000—
Enterprise / GovUnlimitedIncluded

Error responses

The API uses standard HTTP status codes. When something goes wrong you get a JSON body explaining it. Here's what each code means and what to do:

CodeMeaningWhat to do
400Bad request — invalid parameters (bad field, empty term, malformed email or country code)Check your field name, term and format, then retry.
401Unauthorized — missing or invalid tokenConfirm your Authorization: Bearer header and token.
403Forbidden — your plan doesn't include this feature (e.g. full data, or /complete)Upgrade your plan to unlock it.
404Not found — the domain does not exist in DNS (domain reports: "domain_exists": false)Check the spelling of the domain.
429Too many requests — daily rate limit exceededWait for the daily reset or upgrade for a higher quota.
500Internal server errorRetry shortly; if it persists, contact support.

Example error body

{
  "success": false,
  "error": {
    "code": 403,
    "message": "Enterprise feature required",
    "details": "The /complete endpoint is only available for Enterprise license holders"
  }
}

Frequently asked questions

QWhat's the difference between metadata and full data search?

Metadata search returns basic breach information (which companies, groups and dates) without sensitive details. Full data search returns the comprehensive records about the specific data that was compromised.

QWhat's the difference between fulldata and fullstealer?

fulldata searches ransomware breach and traditional breach data. fullstealer searches infostealer logs — stolen credentials, crypto wallets, gaming accounts and other sensitive data pulled from infected machines.

QHow often is the database updated?

Continuously, as new ransomware breaches and infostealer logs are discovered and verified. Updates typically land within 24–48 hours of breach disclosure.

QCan I search for domains instead of specific emails?

Yes. All endpoints support domain queries (e.g. example.com) to find every breach or log affecting that domain.

QWhat is Complete mode and how do I access it?

Complete mode returns all results without pagination — handy for full exports. It's exclusive to Enterprise license holders. Email [email protected] to upgrade.

QHow does pagination work?

Metadata search uses page numbers (up to 20 per page). fulldata and fullstealer use cursor-based pagination via search_after (20 per request). Take the search_after value from the response and pass it as a query parameter for the next batch: ?search_after=2587. When has_next_page is false, you've reached the end. One caveat: that cursor is an integer only while you are not sorting. If you add ?sort=date_desc or date_asc it becomes an opaque string that must be echoed back verbatim along with the same sort value.

QWhy does a country search on fullstealer look different?

A country search returns "mode": "stealers" — results grouped per infected machine. Instead of one isolated row, each machine comes with all of its documents (credentials, cookies, system info, wallets, file list) bundled under its Filename. That gives you the complete profile of every victim machine in that country in one call. All other fields keep their fragments / documents shapes.

QHow do I know which response shape to expect from fullstealer?

Always read the top-level "mode" field. fragments → look in matches (flat list of {URL, USER, PASS, Filename}). documents → look in data (raw docs, real content inside _source). stealers → look in data (one entry per machine, documents bundled inside).

Ready to start building?

Pick a plan to get your API token, or reach out and we'll help you choose.

Get new leak alerts

New ransomware leaks, infostealer dumps and platform updates — straight to your inbox.

We never share your email. Unsubscribe anytime.