# Understanding Holehe’s Module Output JSON Structure: RateLimit, Exists, EmailRecovery, PhoneNumber, and Others

> Learn the Holehe module output JSON structure including rateLimit, exists, emailRecovery, phoneNumber and others. Process results uniformly across 120+ sites with this guide.

- Repository: [Palenath/holehe](https://github.com/megadose/holehe)
- Tags: internals
- Published: 2026-08-20

---

**Holehe modules return a standardized Python dictionary with nine keys—including `rateLimit`, `exists`, `emailrecovery`, `phoneNumber`, and `others`—that enables uniform processing across all 120+ supported sites.**

Holehe is an open-source OSINT tool by megadose that checks whether an email address is registered on websites. Each site-specific module in `holehe/modules/` produces a consistent JSON-style output dictionary. This article breaks down every field in that structure, explains how [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) interprets them, and shows how to consume the data programmatically.

## The Nine Standard Output Keys

Every module in the Holehe repository returns the same dictionary schema. This uniformity lets the core engine aggregate results without site-specific logic.

| Key | Type | Purpose |
|-----|------|---------|
| `name` | `str` | Internal module identifier (e.g., `"twitter"`) |
| `domain` | `str` | Human-readable domain (e.g., `"twitter.com"`) |
| `method` | `str` | Check type—typically `"register"` |
| `frequent_rate_limit` | `bool` | UI hint: site often hits rate limits |
| `rateLimit` | `bool` | **This request** was blocked by rate limiting |
| `exists` | `bool`/`None` | Email found on target site |
| `emailrecovery` | `str`/`None` | Recovered or alternative email address |
| `phoneNumber` | `str`/`None` | Associated phone number, if exposed |
| `others` | `dict`/`None` | Additional metadata (names, dates, custom fields) |

### Where These Keys Are Defined

Module authors set the first four keys as module-level constants:

```python

# From holehe/modules/social_media/twitter.py

name = "twitter"
domain = "twitter.com"
method = "register"
frequent_rate_limit = False

```

The remaining keys are populated dynamically based on the HTTP response.

## rateLimit: Detecting When a Site Blocks You

The **`rateLimit`** boolean indicates whether the remote service returned a rate-limit error (typically HTTP 429) during the check.

In [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py), the `print_result` function handles this flag with special formatting:

```python

# holehe/core.py lines 22-28 (excerpt)

if results["rateLimit"] and args.onlyused == False:
    websiteprint = print_color("[x] " + results["domain"], "yellow", args)

```

When `rateLimit=True`:
- Terminal displays **[x]** in **yellow**
- Module counted as *not-checked* rather than success/failure
- CSV export records `True` for audit purposes

### Rate Limit Implementation in Modules

Modules catch exceptions and set the flag in their `except` blocks. From [`twitter.py`](https://github.com/megadose/holehe/blob/main/twitter.py):

```python

# holehe/modules/social_media/twitter.py lines 30-36

try:
    response = await client.post(...)
    # ... parse response ...

except Exception:
    out.append({
        "name": name,
        "domain": domain,
        "method": method,
        "frequent_rate_limit": frequent_rate_limit,
        "rateLimit": True,  # ← set here

        "exists": None,
        "emailrecovery": None,
        "phoneNumber": None,
        "others": None,
    })

```

## exists: The Core Finding

The **`exists`** field carries the primary OSINT result:

| Value | Meaning | Display |
|-------|---------|---------|
| `True` | Email registered on site | **[+]** in green |
| `False` | Email not found | **[-]** in magenta |
| `None` | Check inconclusive (error/rate-limit) | varies |

The `print_result` logic in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) prioritizes this check:

```python

# holehe/core.py lines 32-38 (excerpt)

elif results["exists"] == False and args.onlyused == False:
    websiteprint = print_color("[-] " + results["domain"], "magenta", args)
elif results["exists"] == True:
    websiteprint = print_color("[+] " + results["domain"], "green", args)

```

## emailrecovery: Extracted Alternative Emails

Some services leak partial recovery email addresses during registration flows. The **`emailrecovery`** field captures these when available.

Notable characteristics:
- Most modules return `None`
- Services with exposed recovery flows (e.g., Adobe historically) may return masked emails like `t***@gmail.com`
- Always validate—format varies by site implementation

## phoneNumber: Leaked Phone Numbers

The **`phoneNumber`** field holds associated phone numbers when services expose them in registration responses. Like `emailrecovery`, this is service-dependent and often `None`.

## others: Extensible Metadata Container

The **`others`** field provides a flexible `dict` for module-specific discoveries. Common uses include:

- `FullName`: Real name associated with account
- `creation_date`: Account registration timestamp
- `approx_lifetime`: Estimated account age
- Custom error messages or debug info

The `print_result` function checks for `error` in this dictionary:

```python

# holehe/core.py lines 28-32

elif "error" in results.keys() and results["error"] and args.onlyused == False:
    # Handle error case

```

## Consuming the JSON Output Programmatically

### Access Raw Results from Python

```python
import asyncio
import httpx
from holehe.core import import_submodules, get_functions, launch_module

async def check_email(email: str, target_module: str):
    """Run a single Holehe module and return its JSON output."""
    modules = import_submodules("holehe.modules")
    all_funcs = get_functions(modules)
    
    # Find target function by name

    func = next(f for f in all_funcs if f.__name__ == target_module)
    
    async with httpx.AsyncClient() as client:
        out = []
        await launch_module(func, email, client, out)
        return out[0]  # The standardized dictionary

# Usage

result = asyncio.run(check_email("test@example.com", "twitter"))
print(result)

```

Example output:

```python
{
    'name': 'twitter',
    'domain': 'twitter.com',
    'method': 'register',
    'frequent_rate_limit': False,
    'rateLimit': False,
    'exists': True,
    'emailrecovery': None,
    'phoneNumber': None,
    'others': None
}

```

### Filter Results by Status

```python
def categorize_results(results_list):
    """Sort Holehe outputs into status categories."""
    categorized = {
        'found': [],
        'not_found': [],
        'rate_limited': [],
        'errors': []
    }
    for r in results_list:
        if r['rateLimit']:
            categorized['rate_limited'].append(r)
        elif 'error' in (r.get('others') or {}):
            categorized['errors'].append(r)
        elif r['exists'] is True:
            categorized['found'].append(r)
        elif r['exists'] is False:
            categorized['not_found'].append(r)
    return categorized

```

## CSV Export Structure

Holehe's CLI exports all fields when using `-C` or `--csv`:

```bash
holehe test@example.com --csv

# Creates: holehe_1724523785_test@example.com_results.csv

```

CSV columns match the dictionary keys exactly:

```csv
name,domain,method,frequent_rate_limit,rateLimit,exists,emailrecovery,phoneNumber,others
twitter,twitter.com,register,False,False,True,,,
facebook,facebook.com,register,False,False,False,,,
instagram,instagram.com,register,True,True,,,,

```

## Key Source Files

| File | Purpose | Critical Lines |
|------|---------|--------------|
| [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) | Output processing, printing, CSV export | `print_result` (~L22-48), `export_csv` |
| [`holehe/modules/social_media/twitter.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/social_media/twitter.py) | Exemplary module implementation | Exception handling (~L30-36) |
| [`holehe/localuseragent.py`](https://github.com/megadose/holehe/blob/main/holehe/localuseragent.py) | Shared user-agent rotation | Used by most modules |
| [`holehe/instruments.py`](https://github.com/megadose/holehe/blob/main/holehe/instruments.py) | Progress bar for parallel execution | Async instrumentation |

## Summary

- **Uniform schema**: Every Holehe module returns the same nine-key dictionary, enabling consistent processing across 120+ sites.
- **Status hierarchy**: `rateLimit` takes precedence, then `error` in `others`, then `exists` True/False for final display determination.
- **Optional enrichment**: `emailrecovery`, `phoneNumber`, and `others` capture additional OSINT when services leak it.
- **Programmatic access**: Import `launch_module` from `holehe.core` to consume results directly without CLI overhead.

## Frequently Asked Questions

### What does `rateLimit=True` mean versus `frequent_rate_limit=True`?

**`rateLimit=True`** means *this specific request* was blocked by the remote service (HTTP 429 or similar). **`frequent_rate_limit`** is a static module property warning that the site often rate-limits—it's used for UI highlighting only. A site can have `frequent_rate_limit=False` but still return `rateLimit=True` on any given run.

### Why is `exists` sometimes `None` instead of `True` or `False`?

`exists` is `None` when the module cannot determine registration status. This occurs when `rateLimit=True` (request blocked), when an unhandled exception occurs, or when the service returns an ambiguous response. The `print_result` function in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) handles `None` as an inconclusive state.

### How do I extract data from the `others` dictionary?

The `others` field is free-form, so safely access it with `.get()`:

```python
others = result.get('others') or {}
full_name = others.get('FullName')
creation_date = others.get('creation_date')

```

Check individual module implementations in `holehe/modules/` to discover what keys each may populate.

### Can I add custom fields to the `others` dictionary in my own module?

Yes. When writing a custom module, populate `others` with any discovery data:

```python
out.append({
    # ... standard keys ...

    'others': {
        'custom_field': extracted_value,
        'metadata': {'source': 'api_endpoint_v2'}
    }
})

```

Ensure values are JSON-serializable for proper CSV export handling.