# How to Implement a New holehe Module: A Complete Developer Guide

> Implement a new holehe module with our developer guide. Create an async Python function, use httpx.AsyncClient, and append results to the out list for effective email registration checks.

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

---

**To implement a new holehe module, create an async Python function with the signature `async def service_name(email, client, out)` inside a Python file under `holehe/modules/<category>/`, use the shared `httpx.AsyncClient` to check email registration status, and append a standardized result dictionary to the `out` list.**

The **megadose/holehe** framework automatically discovers and executes email correlation modules without requiring manual registration. Understanding how to implement a new holehe module involves grasping the auto-discovery mechanism, adhering to the async function contract defined in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py), and structuring HTTP requests using the shared infrastructure.

## Understanding the Auto-Discovery Mechanism

The framework discovers every available check by recursively scanning the `holehe.modules` package at runtime. In [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py), the `import_submodules` function uses `pkgutil.walk_packages` to traverse the directory tree and import every Python file under `holehe/modules/`. 

The `get_functions` helper then extracts callable objects (your async check functions) from each imported module. When the CLI executes, `launch_module` starts each callable in a Trio nursery, passing the shared `httpx.AsyncClient` and output list. Because modules are discovered dynamically, **no registration step is required**—simply dropping a new file into the appropriate sub-directory makes it available immediately.

## Required Function Signature and Parameters

Every holehe module must expose a top-level **async function** with this exact signature:

```python
async def service_name(email, client, out):

```

- **email** (str): The target email address to investigate.
- **client** (httpx.AsyncClient): A shared async HTTP client configured with global timeouts, connection pooling, and proxy settings.
- **out** (list): A mutable list where your function appends result dictionaries.

The file must reside in a sub-package of `holehe/modules/` (e.g., [`holehe/modules/music/spotify.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/music/spotify.py)). The directory must contain an [`__init__.py`](https://github.com/megadose/holehe/blob/main/__init__.py) file (even if empty) for `pkgutil.walk_packages` to recognize it as a package.

## Step-by-Step Implementation Guide

### 1. Define Service Metadata

Begin by declaring identification variables that the printer uses to categorize results:

```python
name = "spotify"
domain = "spotify.com"
method = "register"  # or "login"

frequent_rate_limit = False  # Set True if service aggressively rate-limits

```

### 2. Configure HTTP Headers

Import the random User-Agent generator from [`holehe/localuseragent.py`](https://github.com/megadose/holehe/blob/main/holehe/localuseragent.py) to avoid detection signatures:

```python
from holehe.localuseragent import *

headers = {
    "User-Agent": random.choice(ua["browsers"]["chrome"]),
    "Accept": "application/json, text/plain, */*",
    "Accept-Language": "en-US,en;q=0.5",
    "DNT": "1",
    "Connection": "keep-alive",
}

```

### 3. Execute the Request

Use the injected `client` parameter for all HTTP operations. Do not create separate client instances, as this bypasses the shared configuration:

```python
params = {"email": email, "validate": "1"}
try:
    resp = await client.get(
        "https://api.example.com/check",
        headers=headers,
        params=params,
    )
    data = resp.json()
except Exception:
    # Network or parsing errors trigger rate-limit flag

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

```

### 4. Parse the Response

Interpret the service's response to determine account existence. Logic varies by service—some return JSON status codes, others return specific HTTP status codes or HTML patterns:

```python
if data.get("status") == 20:  # Email found

    exists = True
    rate_limit = False
elif data.get("status") == 1:  # Email not used

    exists = False
    rate_limit = False
else:  # Unknown state or rate limit

    exists = None
    rate_limit = True

```

### 5. Append Standardized Results

Push a dictionary to `out` containing exactly these keys:

```python
out.append({
    "name": name,
    "domain": domain,
    "method": method,
    "frequent_rate_limit": frequent_rate_limit,
    "rateLimit": rate_limit,
    "exists": exists,
    "emailrecovery": None,  # Populate if service exposes recovery email

    "phoneNumber": None,    # Populate if service exposes phone number

    "others": None,         # Populate with additional metadata dict if available

})

```

## Complete Production Module Example

Below is a functional skeleton implementing a fictional service check. Place this file at [`holehe/modules/category/example.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/category/example.py):

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

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

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

    params = {"email": email}
    
    try:
        r = await client.get(
            "https://api.example.com/v1/email_check",
            headers=headers,
            params=params
        )
        data = r.json()
        
        if data.get("registered") is True:
            exists = True
            rate_limit = False
        else:
            exists = False
            rate_limit = False
            
        out.append({
            "name": name, "domain": domain, "method": method,
            "frequent_rate_limit": frequent_rate_limit,
            "rateLimit": rate_limit, "exists": exists,
            "emailrecovery": None, "phoneNumber": None, "others": None,
        })
        
    except Exception:
        out.append({
            "name": name, "domain": domain, "method": method,
            "frequent_rate_limit": frequent_rate_limit,
            "rateLimit": True, "exists": None,
            "emailrecovery": None, "phoneNumber": None, "others": None,
        })

```

## Directory Structure and Deployment

Organize modules by service category to maintain the codebase structure:

- **Choose a category**: `music`, `shopping`, `programming`, `social`, etc.
- **Create the file**: `holehe/modules/<category>/<service>.py`
- **Ensure [`__init__.py`](https://github.com/megadose/holehe/blob/main/__init__.py) exists**: The category folder must contain an [`__init__.py`](https://github.com/megadose/holehe/blob/main/__init__.py) file (can be empty) for `pkgutil.walk_packages` to traverse it.

Because `import_submodules` in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) performs dynamic imports, your module is automatically registered on the next CLI execution without modifying any central registry files.

## Testing and Verification

After creating your module, validate it by running the holehe CLI:

```bash
python -m holehe test@example.com --no-color

```

Your new module will appear in the output list alongside built-in checks. If the module fails to appear, verify:
- The file is saved under `holehe/modules/` with a `.py` extension.
- The directory contains an [`__init__.py`](https://github.com/megadose/holehe/blob/main/__init__.py) file.
- The function definition uses the `async` keyword and accepts exactly three parameters.
- No syntax errors exist in the file (tracebacks appear in the CLI output on failure).

## Summary

- **Auto-discovery**: The `import_submodules` function in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) automatically imports all Python files under `holehe/modules/` using `pkgutil.walk_packages`, requiring no manual registration.
- **Function contract**: Modules must define an async function accepting `(email, client, out)` parameters.
- **HTTP client**: Always use the injected `httpx.AsyncClient` (`client`) to ensure shared timeout, proxy, and connection pool configuration.
- **Result format**: Append dictionaries to `out` containing `name`, `domain`, `method`, `frequent_rate_limit`, `rateLimit`, `exists`, `emailrecovery`, `phoneNumber`, and `others`.
- **File placement**: Create files at `holehe/modules/<category>/<service>.py` with accompanying [`__init__.py`](https://github.com/megadose/holehe/blob/main/__init__.py) files in category directories.

## Frequently Asked Questions

### What is the exact function signature required for a holehe module?

The function must be asynchronous and accept three positional parameters: `email` (the string being investigated), `client` (an `httpx.AsyncClient` instance), and `out` (a list to which you append results). The signature must look like `async def service_name(email, client, out):`. Deviating from this signature prevents `get_functions` in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) from correctly extracting your callable during the discovery phase.

### How does holehe discover new modules automatically without registration?

During initialization, the `import_submodules` function in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) uses `pkgutil.walk_packages` to iterate through every sub-package of `holehe.modules`. It imports each module and `get_functions` introspects them for callable objects. As long as your file is a valid Python module within that tree and contains the properly named async function, it is detected and executed without requiring imports in central configuration files.

### How should I handle rate limiting and errors in my module?

Set `rateLimit` to `True` and `exists` to `None` whenever the service returns HTTP 429, blocks the request, or when network exceptions occur. Wrap your HTTP calls in try/except blocks to catch `httpx` exceptions and parsing errors, appending the rate-limit result dictionary to `out` before returning. Set `frequent_rate_limit = True` in the metadata for services known to aggressively block requests.

### Where should I place my new module file within the repository?

Place the file inside a category folder under `holehe/modules/`, such as [`holehe/modules/music/newservice.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/music/newservice.py). The category folder must contain an [`__init__.py`](https://github.com/megadose/holehe/blob/main/__init__.py) file for Python to recognize it as a package. If no existing category fits the service, create a new directory with an [`__init__.py`](https://github.com/megadose/holehe/blob/main/__init__.py) and place your module there; the auto-discovery mechanism will include it regardless of the directory name.