Why This Matters
Data breaches happen daily. Have I Been Pwned (HIBP) tracks 14+ billion compromised accounts across 800+ breaches. Their API lets you check programmatically — no manual lookups.
Check a Password (Without Sending It)
HIBP uses a k-anonymity model. You send only the first 5 characters of the SHA-1 hash. The API returns all matching hashes. Your password never leaves your machine.
import hashlib
import requests
def check_password(password):
sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
prefix, suffix = sha1[:5], sha1[5:]
r = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}")
for line in r.text.splitlines():
hash_suffix, count = line.split(":")
if hash_suffix == suffix:
return int(count)
return 0
count = check_password("password123")
if count:
print(f"Found in {count:,} breaches! Change it immediately.")
else:
print("Not found in any known breach.")
Output: Found in 126,927 breaches! Change it immediately.
This is completely free, no API key needed, no rate limits.
Check an Email (API Key Required)
Email lookups need a free API key from haveibeenpwned.com:
def check_email(email, api_key):
r = requests.get(
f"https://haveibeenpwned.com/api/v3/breachedaccount/{email}",
headers={"hibp-api-key": api_key},
params={"truncateResponse": "false"}
)
if r.status_code == 200:
breaches = r.json()
return [{"name": b["Name"], "date": b["BreachDate"],
"count": b["PwnCount"]} for b in breaches]
elif r.status_code == 404:
return [] # Not breached
return None
breaches = check_email("test@example.com", "your_key")
for b in breaches or []:
print(f"{b[date]} — {b[name]} ({b[count]:,} accounts)")
Real Use Cases
- Registration forms — warn users if their password is compromised
- Security audits — check employee emails against breach databases
- Password managers — flag reused compromised passwords
- Compliance — NIST SP 800-63B recommends checking passwords against breach lists
- Monitoring — set up alerts when your domain appears in new breaches
Batch Check for Your Domain
def check_domain(domain, api_key):
r = requests.get(
f"https://haveibeenpwned.com/api/v3/breaches",
headers={"hibp-api-key": api_key}
)
breaches = r.json()
# Filter breaches that might affect your domain
recent = [b for b in breaches if b["AddedDate"] > "2025-01-01"]
print(f"{len(recent)} breaches added in 2025+")
for b in recent[:5]:
print(f" {b[Name]}: {b[PwnCount]:,} accounts ({b[BreachDate]})")
Rate Limits
| Endpoint | Key Needed? | Rate Limit |
|---|---|---|
| Password check | No | None (use responsibly) |
| Email check | Yes ($3.50/mo) | 10 req/min |
| Breach list | No | None |
| Paste search | Yes | 10 req/min |
The password API is completely free and unlimited — use it in every registration form.
Add to Your Registration Form
// Frontend: check password on blur
async function isPasswordBreached(password) {
const encoder = new TextEncoder();
const data = encoder.encode(password);
const hashBuffer = await crypto.subtle.digest("SHA-1", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const sha1 = hashArray.map(b => b.toString(16).padStart(2, "0")).join("").toUpperCase();
const prefix = sha1.slice(0, 5);
const suffix = sha1.slice(5);
const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
const text = await res.text();
return text.includes(suffix);
}
I write about APIs, security tools, and developer productivity. More on GitHub.
More from me: 10 Dev Tools I Use Daily | 77 Scrapers on a Schedule | 150+ Free APIs
Top comments (0)