← Back to Quick Start

Check Passwords Against Breach Databases (Safely)

Use k-anonymity to check if a password has been compromised — without ever sending the plaintext password over the wire.

What you'll learn

  • How k-anonymity keeps passwords private during breach checks
  • How to SHA-1 hash a password locally before sending the prefix
  • How to interpret breach count and prevalence data
  • How to integrate breach checks into registration and password-change flows

Prerequisites

How k-anonymity protects your users

You never send the full password or even the full hash to the API. Here is how it works:

  1. Hash the password locally using SHA-1 (e.g., "password" becomes 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8).
  2. Send only the first 5 characters of the hash (5BAA6) as the prefix.
  3. The API returns all known breach hashes that share that prefix.
  4. Your code checks locally whether the full hash is in the returned set.

This means the API never learns which password you are checking. The server cannot distinguish your query from hundreds of other hashes sharing the same prefix.

Step 1: Your first call

First, hash the password locally. Let's check the famously breached password "password":

# Hash locally (never send plaintext!)
echo -n "password" | sha1sum
# Output: 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8

# Send the full SHA-1 hash to the API
curl -s -X POST /api/v1/check \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "sha1Hash": "5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8"
  }' | python3 -m json.tool

Expected response:

{
  "sha1Prefix": "5BAA6",
  "found": true,
  "breachCount": 9545824,
  "prevalence": "EXTREMELY_COMMON",
  "recommendation": "REJECT",
  "message": "This password has appeared in 9,545,824 data breaches. It should never be used.",
  "processingTimeMs": 18
}

Step 2: Understanding the response

found Whether the exact hash was found in breach databases.
breachCount Number of times this password appeared across all known breaches.
prevalence Human-readable severity: EXTREMELY_COMMON, VERY_COMMON, COMMON, UNCOMMON, RARE.
recommendation REJECT (force password change), WARN (suggest change), or ACCEPT (not found in breaches).

Step 3: Common use cases

Checking a clean password

# Hash a strong password locally
echo -n "j&4Kx!mP9qR2vZ#w" | sha1sum
# Output: a3f2b8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9

curl -s -X POST /api/v1/check \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"sha1Hash": "A3F2B8C1D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9"}'
{
  "sha1Prefix": "A3F2B",
  "found": false,
  "breachCount": 0,
  "prevalence": "NOT_FOUND",
  "recommendation": "ACCEPT",
  "message": "This password has not been found in any known data breaches.",
  "processingTimeMs": 12
}

Using k-anonymity prefix only

You can also send just the first 5 hex characters if you prefer to do the full-hash comparison client-side:

curl -s -X POST /api/v1/check \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"sha1Prefix": "5BAA6"}'

The API returns all matching suffixes so your client can compare locally — the server never learns the full hash.

Step 4: Handling errors

Invalid hash format

curl -s -X POST /api/v1/check \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"sha1Hash": "ZZZZ_NOT_HEX"}'
{
  "error": "VALIDATION_ERROR",
  "message": "sha1Hash: must be a valid 40-character hexadecimal SHA-1 hash",
  "status": 400
}

Missing required field

{
  "error": "VALIDATION_ERROR",
  "message": "Either sha1Hash or sha1Prefix must be provided",
  "status": 400
}

Rate limiting

Breach checks are rate-limited to prevent enumeration attacks. Respect 429 responses and the Retry-After header.

Step 5: Integration tips

Python registration check

import hashlib
import requests

API_BASE = ""
HEADERS = {
    "Content-Type": "application/json",
    "X-API-Key": "YOUR_API_KEY"
}

def is_password_breached(plaintext_password):
    """Check if a password appears in breach databases.
    The plaintext is NEVER sent to the API."""
    sha1_hash = hashlib.sha1(
        plaintext_password.encode("utf-8")
    ).hexdigest().upper()

    resp = requests.post(
        f"{API_BASE}/api/v1/check",
        json={"sha1Hash": sha1_hash},
        headers=HEADERS, verify=False
    )
    resp.raise_for_status()
    data = resp.json()
    return data["found"], data.get("breachCount", 0)

# Usage in registration
password = input("Choose a password: ")
found, count = is_password_breached(password)
if found:
    print(f"This password appeared in {count:,} breaches. Choose another.")
else:
    print("Password is safe to use.")

Security best practices

Next steps