# How to Add a New Website Module to holehe Following the Existing Module Pattern

> Easily add a new website module to holehe. Learn the simple pattern: create an async function in the modules directory with no registration needed. Integrate seamlessly!

- Repository: [Palenath/holehe](https://github.com/megadose/holehe)
- Tags: how-to-guide
- Published: 2026-08-15

---

**Creating a new website module for holehe requires implementing a single async function with the signature `async def <module_name>(email, client, out)` in a Python file under `holehe/modules/<category>/`, with no registration step needed.**

holehe automatically discovers every check routine by walking the package `holehe.modules` using the `import_submodules` helper in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py). This means you can add a new website module simply by creating a properly structured Python file—the framework handles the rest. This guide walks you through the exact pattern used by existing modules like the Gmail check in [`holehe/modules/mails/google.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/mails/google.py).

## Understanding the Module Discovery System

In [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) at lines 37-47, the `import_submodules` function recursively imports all Python files beneath `holehe.modules`:

```python
def import_submodules(package, recursive=True):
    """Import all submodules of a package."""
    results = {}
    for _, name, is_pkg in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
        results[name] = importlib.import_module(name)
        if recursive and is_pkg:
            results.update(import_submodules(sys.modules[name]))
    return results

```

Because of this automatic discovery, you never need to register or list your new module manually. Placing a valid `.py` file in the correct location is sufficient.

## Required Function Signature

Every module must export exactly one async function with this signature:

```python
async def <module_name>(email, client, out):

```

| Parameter | Type | Purpose |
|-----------|------|---------|
| `email` | `str` | The email address being verified |
| `client` | `httpx.AsyncClient` | Shared async HTTP client—**always reuse this**, never create your own |
| `out` | `list` | Shared result list to append your dictionary to |

## The Five-Step Module Template

Follow this structure based on existing modules like [`holehe/modules/mails/google.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/mails/google.py):

### 1. Define Service Metadata

Set four standardized variables identifying the service:

```python
name = "examplemail"
domain = "examplemail.com"
method = "register"  # or "password recovery"

frequent_rate_limit = False

```

### 2. Prepare Request Headers

Import random user-agents from `holehe.localuseragent`:

```python
from holehe.localuseragent import *

headers = {
    "User-Agent": random.choice(ua["browsers"]["firefox"]),
    "Accept": "*/*",
    # Add service-specific headers as needed

}

```

### 3. Perform the HTTP Request

Use the provided `client` for all network operations:

```python
try:
    response = await client.post(
        "https://api.examplemail.com/check",
        headers=headers,
        json={"email": email},
    )
except Exception:
    # Network-level failure → treat as rate-limited

    out.append({
        "name": name,
        "domain": domain,
        "method": method,
        "frequent_rate_limit": frequent_rate_limit,
        "rateLimit": True,
        "exists": False,
        "emailrecovery": None,
        "phoneNumber": None,
        "others": None,
    })
    return

```

### 4. Parse the Response

Analyze status codes, JSON, or HTML to determine account existence:

```python
data = response.json()

if "account_exists" in data:
    exists = data["account_exists"]
elif "error" in data and "rate" in data["error"].lower():
    # Service-side rate limit

    out.append({...rateLimit: True...})
    return
else:
    # Unknown response format

    exists = False

```

### 5. Append Standardized Result

The output dictionary must contain exactly these keys:

| Key | Type | Description |
|-----|------|-------------|
| `name` | `str` | Module name |
| `domain` | `str` | Service domain |
| `method` | `str` | Check method used |
| `frequent_rate_limit` | `bool` | Whether service commonly rate-limits |
| `rateLimit` | `bool` | Whether this request hit a limit |
| `exists` | `bool` | Whether email is registered |
| `emailrecovery` | `str` or `None` | Recovery email if exposed |
| `phoneNumber` | `str` or `None` | Phone number if exposed |
| `others` | `dict` or `None` | Additional metadata |

```python
out.append({
    "name": name,
    "domain": domain,
    "method": method,
    "frequent_rate_limit": frequent_rate_limit,
    "rateLimit": False,
    "exists": exists,
    "emailrecovery": data.get("recovery_email"),
    "phoneNumber": data.get("phone"),
    "others": None,
})

```

## Complete Example: Adding an ExampleMail Module

Create [`holehe/modules/mails/examplemail.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/mails/examplemail.py):

```python
from holehe.core import *
from holehe.localuseragent import *

async def examplemail(email, client, out):
    name = "examplemail"
    domain = "examplemail.com"
    method = "register"
    frequent_rate_limit = False

    headers = {
        "User-Agent": random.choice(ua["browsers"]["chrome"]),
        "Accept": "application/json",
        "X-Requested-With": "XMLHttpRequest",
    }

    try:
        r = await client.post(
            "https://api.examplemail.com/v1/users/check",
            headers=headers,
            json={"email": email},
            timeout=10,
        )
    except Exception:
        out.append({
            "name": name,
            "domain": domain,
            "method": method,
            "frequent_rate_limit": frequent_rate_limit,
            "rateLimit": True,
            "exists": False,
            "emailrecovery": None,
            "phoneNumber": None,
            "others": None,
        })
        return

    try:
        data = r.json()
    except ValueError:
        # Invalid JSON response

        out.append({
            "name": name,
            "domain": domain,
            "method": method,
            "frequent_rate_limit": frequent_rate_limit,
            "rateLimit": True,
            "exists": False,
            "emailrecovery": None,
            "phoneNumber": None,
            "others": None,
        })
        return

    # Check for rate limiting in response

    if r.status_code == 429 or data.get("status") == "rate_limited":
        out.append({
            "name": name,
            "domain": domain,
            "method": method,
            "frequent_rate_limit": frequent_rate_limit,
            "rateLimit": True,
            "exists": False,
            "emailrecovery": None,
            "phoneNumber": None,
            "others": None,
        })
        return

    exists = data.get("user_exists", False)

    out.append({
        "name": name,
        "domain": domain,
        "method": method,
        "frequent_rate_limit": frequent_rate_limit,
        "rateLimit": False,
        "exists": exists,
        "emailrecovery": data.get("recovery_email"),
        "phoneNumber": data.get("verified_phone"),
        "others": {"username": data.get("suggested_username")} if "suggested_username" in data else None,
    })

```

## Step-by-Step Checklist for Adding a Module

- Choose an appropriate category folder under `holehe/modules/` (e.g., `mails`, `social`, `cms`, `forum`, `shopping`)
- Create a new `.py` file with a lowercase name matching your function name
- Implement the async function with the exact signature `async def <name>(email, client, out)`
- Always use the provided `client` parameter—never instantiate `httpx.AsyncClient` inside your module
- Return results using the standard dictionary keys used by all other modules
- Test your module with `holehe test@example.com` and verify output formatting

## Key Files in the holehe Source Code

| Path | Role |
|------|------|
| [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) (lines 37-47) | Contains `import_submodules` which auto-discovers all modules |
| [`holehe/modules/mails/google.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/mails/google.py) | Reference implementation for email-based services |
| [`holehe/localuseragent.py`](https://github.com/megadose/holehe/blob/main/holehe/localuseragent.py) | Supplies `ua` dictionary with browser user-agent strings |
| [`holehe/__init__.py`](https://github.com/megadose/holehe/blob/main/holehe/__init__.py) | Package entry point—no modifications needed |

## Testing Your New Module

After creating your file, verify discovery and functionality:

```bash

# Test that the module appears in the module list

python -c "from holehe.core import import_submodules; import holehe.modules; print(list(import_submodules(holehe.modules).keys()))"

# Run a live test

holehe -C your-test-email@example.com

```

Expected output should show `[+] examplemail.com` (green, account exists) or `[-] examplemail.com` (magenta, not found).

## Common Mistakes to Avoid

- **Creating a new HTTP client**: Always use the `client` parameter to enable connection pooling and proper async handling
- **Missing keys in output**: The result dictionary must contain all nine standardized keys or downstream formatting will break
- **Synchronous requests**: The function must be `async` and use `await` on all network calls
- **Wrong file location**: Files outside `holehe/modules/` or without the correct function name won't be discovered

## Summary

- holehe discovers modules automatically via `import_submodules` in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py)—no registration required
- Each module exports one `async def` function with signature `(email, client, out)`
- Five required steps: define metadata, set headers, make request, parse response, append standardized result
- Always reuse the provided `httpx.AsyncClient` and return all nine required dictionary keys
- Place new files in `holehe/modules/<category>/<module_name>.py` following lowercase naming conventions

## Frequently Asked Questions

### What happens if I forget a key in the output dictionary?

Downstream code in holehe expects exactly nine keys in every result dictionary. Missing keys will cause `KeyError` exceptions during result formatting and display. Always include `name`, `domain`, `method`, `frequent_rate_limit`, `rateLimit`, `exists`, `emailrecovery`, `phoneNumber`, and `others`—use `None` for values you cannot populate.

### Can I create a new category folder instead of using existing ones?

Yes. The `import_submodules` function recursively walks all subdirectories. Create `holehe/modules/customservice/` and place your module there—holehe will discover it automatically. Use lowercase, no-spaces folder names for consistency with existing code.

### Why must I use the provided `client` instead of creating my own?

The `client` parameter is a pre-configured `httpx.AsyncClient` that shares connection pools, follows redirects consistently, and respects timeout settings across all modules. Creating separate clients would exhaust file descriptors, lose cookie persistence, and break the concurrent execution model that makes holehe performant.