Unofficial Python client for the Epstein Exposed public API.
Search persons, documents, flight logs, and emails from the Epstein case files — programmatically, with both sync and async interfaces.
Disclaimer: Inclusion in the Epstein Exposed database does not imply guilt or wrongdoing. All data is derived from publicly released government records, court filings, and verified reporting. Attribution to epsteinexposed.com is requested.
pip install epsteinexposedfrom epsteinexposed import EpsteinExposed
with EpsteinExposed() as client:
# Search persons
persons = client.search_persons(q="clinton", category="politician")
for p in persons.data:
print(f"{p.name} — {p.stats.flights} flights, {p.stats.documents} docs")
# Get person detail
detail = client.get_person("bill-clinton")
print(detail.bio, detail.aliases)
# Search documents
docs = client.search_documents(q="little st james", source="court-filing")
for d in docs.data:
print(d.title, d.source_url)
# Search flights
flights = client.search_flights(passenger="trump", year=1997)
for f in flights.data:
print(f"{f.date}: {f.origin} → {f.destination}")
# Cross-type search
results = client.search(q="wexner trust", type="documents")
print(len(results.documents.results), "document hits")import asyncio
from epsteinexposed import AsyncEpsteinExposed
async def main():
async with AsyncEpsteinExposed() as client:
persons = await client.search_persons(q="maxwell")
for p in persons.data:
print(p.name)
asyncio.run(main())The upstream API is behind Cloudflare bot protection, which blocks standard HTTP clients (httpx, requests) with a 403 challenge page. This library uses curl_cffi to impersonate a Chrome browser's TLS fingerprint, bypassing the challenge transparently.
client = EpsteinExposed(impersonate="chrome") # default| Endpoint | Method | Client Method |
|---|---|---|
GET /api/v1/persons |
Search/filter persons | search_persons() |
GET /api/v1/persons/:slug |
Person detail | get_person() |
GET /api/v1/documents |
Search documents (FTS5) | search_documents() |
GET /api/v1/flights |
Search flight logs | search_flights() |
GET /api/v1/search |
Cross-type search | search() |
The upstream API enforces per-IP rate limits using a sliding window:
| Endpoints | Limit |
|---|---|
/persons, /persons/:slug, /documents, /flights |
60 requests / minute |
/search |
30 requests / minute |
Exceeding the limit returns HTTP 429 and raises EpsteinExposedRateLimitError.
All responses are parsed into typed Pydantic models:
PaginatedResponse[Person],PaginatedResponse[Document],PaginatedResponse[Flight]PersonDetail(extended person with bio, aliases, black book status)SearchResults(documents + emails)PaginationMeta(total, page, per_page, timestamp)
from epsteinexposed import EpsteinExposed, EpsteinExposedRateLimitError
client = EpsteinExposed()
try:
client.search_persons(q="test")
except EpsteinExposedRateLimitError:
print("Rate limited — back off and retry")| Exception | HTTP Code |
|---|---|
EpsteinExposedValidationError |
400 |
EpsteinExposedNotFoundError |
404 |
EpsteinExposedRateLimitError |
429 |
EpsteinExposedServerError |
5xx |
EpsteinExposedAPIError |
(base) |
The package exposes several constants useful for parameter validation:
| Constant | Values |
|---|---|
PERSON_CATEGORIES |
politician, business, royalty, celebrity, associate, legal, academic, socialite, military-intelligence, other |
DOCUMENT_SOURCES |
court-filing, doj-release, fbi, efta |
DOCUMENT_CATEGORIES |
deposition, testimony, correspondence |
SEARCH_TYPES |
documents, emails |
from epsteinexposed._constants import PERSON_CATEGORIES, DOCUMENT_SOURCESgit clone https://github.com/guilyx/epsteinexposed.git
cd epsteinexposed
make install-dev
make test # unit tests (mocked)
make lintRun integration tests against the real API (rate-limit aware):
pytest -m integration --no-covSee CONTRIBUTING.md for development setup, code standards, and how to submit changes.
Full docs at guilyx.github.io/epsteinexposed — built with Vite + React + Tailwind CSS. Run locally with make docs.
MIT — see LICENSE.