Skip to content
Finowl

Python · Email breach checks

Check email breaches in Python with requests

The email breach endpoint takes one address and returns a breached flag plus a list of breach records. In Python that list arrives as plain dictionaries, so you can loop over it, filter on IsVerified, or turn DataClasses into a set to see whether passwords or phone numbers were exposed. The script below is a command-line checker you can drop into a cron job or call from a management command.

Error handling stays explicit. A 400 means the address failed validation (display names, quoted local parts and IP-address domains are rejected), a 401 points to a missing or wrong key, and a rate-limited 429 tells you how many seconds to wait in Retry-After. A 429 with quota_exceeded means you are out of credits, so the script stops instead of looping.

Steps

  1. 1

    Install requests with pip install requests inside your virtual environment.

  2. 2

    Create an API key in the Finowl dashboard and export it as FINOWL_API_KEY.

  3. 3

    Save the script as check_email.py and run python3 check_email.py alex@example.com.

  4. 4

    Read each breach's Title, BreachDate and DataClasses to decide what to tell the user.

  5. 5

    Move check_email into your app and call it from a signup view or a scheduled task.

Install
pip install requests
Check email breaches in Python with requests
import os
import sys
import time

import requests

API_URL = "https://api.finowl.co/v1/breaches/email"
HEADERS = {
    "Authorization": f"Bearer {os.environ['FINOWL_API_KEY']}",
    "Content-Type": "application/json",
}


def error_code(resp):
    try:
        return resp.json()["error"]["code"]
    except (ValueError, KeyError, TypeError):
        return "unknown_error"


def check_email(email, attempts=3):
    for _ in range(attempts):
        resp = requests.post(API_URL, json={"email": email}, headers=HEADERS, timeout=10)
        if resp.status_code == 200:
            return resp.json()
        code = error_code(resp)
        if resp.status_code == 400:
            raise ValueError(f"Finowl rejected the address: {code}")
        if resp.status_code == 401:
            raise PermissionError("Unauthorized: check FINOWL_API_KEY")
        if resp.status_code == 429 and code == "rate_limited":
            time.sleep(int(resp.headers.get("Retry-After", "1")))
            continue
        raise RuntimeError(f"Finowl returned {resp.status_code}: {code}")
    raise RuntimeError("Still rate limited after several attempts")


if __name__ == "__main__":
    result = check_email(sys.argv[1] if len(sys.argv) > 1 else "alex@example.com")
    if not result["breached"]:
        print(f"{result['email']} does not appear in any known breach")
    for breach in result["breaches"]:
        flag = "" if breach["IsVerified"] else " [unverified]"
        print(f"{breach['Title']} ({breach['BreachDate']}){flag}")
        print("  exposed: " + ", ".join(breach["DataClasses"]))

IN PRODUCTION

Production tips

  • Treat unverified breaches differently from verified ones. Filtering on IsVerified lets you show a softer warning, or skip the notification entirely, when a record has not been confirmed.

  • When rechecking a large customer list from Celery, keep a single requests.Session per worker process and space calls out so the whole fleet stays under 60 requests per minute.

  • Store only what you need, such as breach Name and BreachDate, so a later recheck can detect new breaches without keeping the full Description text in your database.

Each lookup costs 1 credit, with 100 free credits every month. See pricing and error codes.

A FEW GOOD QUESTIONS

Common questions

Check email breaches in Python with requests

Grab an API key, paste the example and run it. 100 free credits every month.