TraceFind API
Run email, username and phone searches from your own code, and read your search history and PDF reports. The API uses the same search engine as the site and is billed the same way: 5 credits per search, refunded when nothing is found.
- Base URL
{API base URL}- Format
- JSON over HTTPS
- Auth
- Bearer token from your dashboard
Endpoints at a glance
| Endpoint | Scope | What it does |
|---|---|---|
POST /api/v1/searches | search | Start a search, optionally waiting for the result |
GET /api/v1/searches/{id} | read | Status of one API search, with results once done |
GET /api/v1/searches | read | Your most recent API searches |
GET /api/v1/reports | read | Your full search history (site and API), paged |
GET /api/v1/reports/{report_id} | read | One report with its results |
GET /api/v1/reports/{report_id}/pdf | read | The report as a PDF file |
GET /api/v1/account | read | Credits left, token details and limits |
"read" endpoints work with both token scopes; only POST /api/v1/searches needs a search token.
Using an AI assistant such as Claude, Cursor or VS Code? Connect it to the TraceFind MCP server and it can run these searches as tools, with the same token.
Quick start
- Buy credits. API searches spend purchased credits only (the free signup credits are for the website). Buy credits.
- Create a token. Go to Account → API, name the token after the tool that will use it, pick the Search and read scope and copy the token. It is shown once.
- Store it as an environment variable so it never ends up in your source code:
export TRACEFIND_TOKEN="tf_live_…"
- Run your first search. With
"wait": 90the request holds until the search finishes (most do within a minute). If it is still running after that, the response says"status": "running"and you pollGET /api/v1/searches/{id}.
curl -X POST {API base URL}/api/v1/searches \
-H "Authorization: Bearer $TRACEFIND_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type": "email", "query": "someone@example.com", "wait": 90}'
# pip install requests
import os, time, requests
API = "{API base URL}/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['TRACEFIND_TOKEN']}"}
search = requests.post(f"{API}/searches", headers=HEADERS, timeout=130,
json={"type": "username", "query": "johndoe", "wait": 90})
search.raise_for_status()
search = search.json()
while search["status"] in ("queued", "running"):
time.sleep(5)
search = requests.get(f"{API}/searches/{search['id']}", headers=HEADERS, timeout=30).json()
print(search["status"], search["summary"])
for result in search.get("results", []):
print(result["type"], result.get("sitename") or result.get("name"))
// Node 18+ (built-in fetch), as an ES module
const API = '{API base URL}/api/v1';
const headers = {
Authorization: `Bearer ${process.env.TRACEFIND_TOKEN}`,
'Content-Type': 'application/json',
};
let res = await fetch(`${API}/searches`, {
method: 'POST',
headers,
body: JSON.stringify({ type: 'phone', query: '+4915112345678', wait: 90 }),
});
if (!res.ok) throw new Error((await res.json()).detail);
let search = await res.json();
while (['queued', 'running'].includes(search.status)) {
await new Promise((r) => setTimeout(r, 5000));
search = await (await fetch(`${API}/searches/${search.id}`, { headers })).json();
}
console.log(search.status, search.summary);
Keep tokens on your server. Never put one in a web page, mobile app or public repository: anyone who has it can spend your credits.
Authentication
Every request needs an API token in the Authorization header:
Authorization: Bearer tf_live_3hV0q…
Tokens start with tf_live_ followed by 43 random characters. Create, list and revoke them on your dashboard under Account → API.
| Scope | Can do | Use it for |
|---|---|---|
searchSearch and read | Start searches (spends credits), read searches, reports, PDFs and the account | Scripts and integrations that run lookups |
readRead only | Everything except starting searches | Dashboards, exports and backups of your history |
- Shown once. Only a SHA-256 hash of the token is stored, so we cannot show it to you again. Lost it? Revoke it and create a new one.
- Expiry. A token can expire after 30, 90 or 365 days, or never. Revoking takes effect on the very next request.
- Up to 10 active tokens per account. Use one per tool, so you can cut one off without touching the others; the dashboard shows when each was last used.
- Not your login. The API never accepts your account ID and never returns it, so a leaked token cannot be used to take over your account.
A missing, wrong, revoked or expired token returns 401 with a WWW-Authenticate: Bearer header:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
{"detail": "This API token is invalid or has been revoked."}
Searches
An API search runs the same checks as the website. It is queued, picked up by a worker within a second or two, and saved to your history as a report when it finishes.
POST/api/v1/searches
Starts a search. Needs a search token and at least 5 purchased credits.
Request body (JSON)
| Field | Type | Description |
|---|---|---|
type required | string | email, username or phone (case-insensitive). |
query required | string | What to search, 1–320 characters. Phone numbers in international format (+4915112345678); spaces and dashes are fine. Usernames: 2–64 characters of letters, digits, ., _ and -. |
wait | integer | Seconds (0–120) to hold the request until the search finishes. Default 0: return at once with "status": "queued". Set your HTTP client's timeout a little higher than this. |
curl -X POST {API base URL}/api/v1/searches \
-H "Authorization: Bearer $TRACEFIND_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type": "phone", "query": "+49 151 12345678"}'
r = requests.post(f"{API}/searches", headers=HEADERS,
json={"type": "phone", "query": "+49 151 12345678"})
search = r.json() # status "queued"; poll GET /searches/{id}
const search = await (await fetch(`${API}/searches`, {
method: 'POST',
headers,
body: JSON.stringify({ type: 'phone', query: '+49 151 12345678' }),
})).json(); // status "queued"; poll GET /searches/{id}
201 Created{
"id": "4CQL0DSL4KCC",
"type": "phone",
"query": "+4915112345678",
"status": "queued",
"created_at": "2026-11-01T12:34:05.118000+00:00",
"finished_at": null,
"found": false,
"report_id": null,
"summary": null,
"error": null,
"credits_spent": 0
}
GET/api/v1/searches/{id}
The search's current status, and its results once it is done. Poll every 3–5 seconds; email searches take about 45 seconds, usernames about 30 and phone numbers about 15. Only searches started through the API are found here; for searches run on the website use reports.
curl {API base URL}/api/v1/searches/4CQL0DSL4KCC \
-H "Authorization: Bearer $TRACEFIND_TOKEN"
search = requests.get(f"{API}/searches/4CQL0DSL4KCC", headers=HEADERS).json()
const search = await (await fetch(`${API}/searches/4CQL0DSL4KCC`, { headers })).json();
200 OK{
"id": "4CQL0DSL4KCC",
"type": "phone",
"query": "+4915112345678",
"status": "done",
"created_at": "2026-11-01T12:34:05.118000+00:00",
"finished_at": "2026-11-01T12:34:17.402000+00:00",
"found": true,
"report_id": "M8YO18VH3BIB",
"summary": {"hits": 3, "messengers": ["WhatsApp", "Signal", "Facebook"],
"country": "Germany", "carrier": "Vodafone"},
"error": null,
"credits_spent": 5,
"results": [
{"type": "lookup", "data": {"Valid": true, "Location": "Germany", "Carrier": "Vodafone", "Number type": "Mobile"}},
{"type": "whatsapp", "data": {"exists": true}},
{"type": "signal", "data": {"exists": true}},
…
]
}
GET/api/v1/searches
Your most recent API searches, newest first, without results.
| Query | Type | Description |
|---|---|---|
limit | integer | 1–100, default 20. |
curl "{API base URL}/api/v1/searches?limit=5" -H "Authorization: Bearer $TRACEFIND_TOKEN"
200 OK{"searches": [ {"id": "4CQL0DSL4KCC", "type": "phone", "status": "done", …}, … ]}
The search object
| Field | Type | Description |
|---|---|---|
id | string | 12 characters, A–Z and 0–9. |
type, query | string | What was searched; query is normalised (e.g. phone numbers without spaces). |
status | string | See the table below. |
created_at, finished_at | string | null | ISO 8601 timestamps in UTC. |
found | boolean | Whether anything turned up. If false the search was free. |
report_id | string | null | The report in your history, for reports and the PDF. |
summary | object | null | A few headline numbers, see below. |
error | string | null | Why a search failed. |
credits_spent | integer | 5 when something was found, otherwise 0. |
results | array | Only on GET /searches/{id} and POST once the status is done. See result format. |
| status | Meaning |
|---|---|
queued | Waiting for a worker; usually a second or two. |
running | Searching. |
done | Finished. found says whether anything turned up; results holds it. |
failed | The search did not finish (see error). Nothing is charged; start it again. |
waiting_for_credits | Your credits ran out before it could start. It continues once you buy credits. |
cancelled | Stopped before it ran. |
The summary object
| Search | Fields |
|---|---|
hits, breaches, accounts, breach_names (up to 8), account_names (up to 8), deliverable (true, false or null if unknown) | |
| username | hits, profiles, sites (up to 12 site names) |
| phone | hits, messengers (e.g. ["WhatsApp", "Telegram"]), country, carrier |
Reports
Every search you have run, on the website or through the API, is saved as a report in your history.
GET/api/v1/reports
Your history, newest first.
| Query | Type | Description |
|---|---|---|
type | string | all (default), email, username or phone. |
limit | integer | 1–50, default 20. |
offset | integer | How many reports to skip, default 0. Page until a response has fewer than limit reports. |
results | boolean | true includes each report's results; otherwise you get a result_count instead (much smaller). |
curl "{API base URL}/api/v1/reports?type=email&limit=10" \
-H "Authorization: Bearer $TRACEFIND_TOKEN"
page = requests.get(f"{API}/reports", headers=HEADERS,
params={"type": "email", "limit": 10}).json()
const page = await (await fetch(`${API}/reports?type=email&limit=10`, { headers })).json();
200 OK{
"reports": [
{
"report_id": "K2DD8QW1ZJ4M",
"timestamp": "2026-10-30T09:12:44.507000+00:00",
"search_type": "email",
"search_term": "someone@example.com",
"tier": "paid",
"locked": false,
"result_count": 14
}
],
"limit": 10,
"offset": 0
}
GET/api/v1/reports/{report_id}
One report with its results. A report from the free trial on an account that has never bought credits comes back as a preview, as on the site: "locked": true, an empty results list and a preview object with counts per category. Buying credits unlocks it.
curl {API base URL}/api/v1/reports/K2DD8QW1ZJ4M -H "Authorization: Bearer $TRACEFIND_TOKEN"
200 OK{
"report_id": "K2DD8QW1ZJ4M",
"timestamp": "2026-10-30T09:12:44.507000+00:00",
"search_type": "email",
"search_term": "someone@example.com",
"tier": "paid",
"locked": false,
"results": [
{"type": "social", "name": "Spotify", "url": "https://spotify.com", "others": {…}},
{"type": "leaks", "data": [{"Name": "ExampleBreach", …}]},
{"type": "email_verify", "data": {"available": true, "reachable": true, …}},
…
]
}
GET/api/v1/reports/{report_id}/pdf
The report as a PDF (application/pdf, file name tracefind_{report_id}.pdf), the same document as the site's export. See the example report. Free previews return 402.
curl -o report.pdf {API base URL}/api/v1/reports/K2DD8QW1ZJ4M/pdf \
-H "Authorization: Bearer $TRACEFIND_TOKEN"
r = requests.get(f"{API}/reports/K2DD8QW1ZJ4M/pdf", headers=HEADERS, timeout=60)
r.raise_for_status()
open("report.pdf", "wb").write(r.content)
import { writeFile } from 'node:fs/promises';
const res = await fetch(`${API}/reports/K2DD8QW1ZJ4M/pdf`, { headers });
await writeFile('report.pdf', Buffer.from(await res.arrayBuffer()));
Account
GET/api/v1/account
Your purchased credits, how many searches they cover, and the calling token's name, scope, expiry and limits. A cheap way to check a token works.
curl {API base URL}/api/v1/account -H "Authorization: Bearer $TRACEFIND_TOKEN"
200 OK{
"credits": 135,
"searches_left": 27,
"search_cost": 5,
"token": {"name": "CRM sync", "scope": "search", "expires_at": null},
"limits": {"requests_per_minute": 60, "running_searches": 5}
}
MCP server
TraceFind is also an MCP server, so AI assistants and agents can use it directly. Connect Claude, Cursor, VS Code or any other MCP client, and the assistant can run searches and read your reports when you ask it to, for example "check which accounts are linked to jane.doe@example.com".
- Server URL
{API base URL}/mcp- Transport
- Streamable HTTP
- Auth
- The same API token
It is the API above in another form: the same tokens and scopes, the same rate limit, and the same billing (5 credits per search, refunded when nothing is found). Give the assistant a token of its own, so you can see when it was last used and revoke it on its own. A read token lets the assistant read your history without being able to spend credits.
Connect your client
# In a terminal, with TRACEFIND_TOKEN set as in the quick start.
claude mcp add --transport http tracefind {API base URL}/mcp \
--header "Authorization: Bearer $TRACEFIND_TOKEN"
{
"mcpServers": {
"tracefind": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"{API base URL}/mcp",
"--header",
"Authorization:${AUTH_HEADER}"
],
"env": {
"AUTH_HEADER": "Bearer tf_live_…"
}
}
}
}
{
"mcpServers": {
"tracefind": {
"url": "{API base URL}/mcp",
"headers": {
"Authorization": "Bearer tf_live_…"
}
}
}
}
{
"servers": {
"tracefind": {
"type": "http",
"url": "{API base URL}/mcp",
"headers": {
"Authorization": "Bearer tf_live_…"
}
}
}
}
{
"mcpServers": {
"tracefind": {
"serverUrl": "{API base URL}/mcp",
"headers": {
"Authorization": "Bearer tf_live_…"
}
}
}
}
# ~/.codex/config.toml, with the token in the TRACEFIND_TOKEN environment variable.
[mcp_servers.tracefind]
url = "{API base URL}/mcp"
bearer_token_env_var = "TRACEFIND_TOKEN"
{
"mcpServers": {
"tracefind": {
"httpUrl": "{API base URL}/mcp",
"headers": {
"Authorization": "Bearer tf_live_…"
}
}
}
}
{
"mcpServers": {
"tracefind": {
"type": "streamableHttp",
"url": "{API base URL}/mcp",
"headers": {
"Authorization": "Bearer tf_live_…"
}
}
}
}
URL: {API base URL}/mcp
Transport: Streamable HTTP
Authorization: Bearer tf_live_…
- Claude Code: run the command in a terminal.
- Claude Desktop: Settings → Developer → Edit Config (claude_desktop_config.json), then restart Claude. Needs Node.js.
- Cursor: ~/.cursor/mcp.json (or .cursor/mcp.json in a project).
- VS Code: .vscode/mcp.json in your project, or "MCP: Open User Configuration" for all projects.
- Windsurf: ~/.codeium/windsurf/mcp_config.json, then refresh the MCP servers in Cascade.
- Codex CLI: ~/.codex/config.toml, with the token in the TRACEFIND_TOKEN environment variable.
- Gemini CLI: ~/.gemini/settings.json (or .gemini/settings.json in a project).
- Cline: Cline → MCP Servers → Configure MCP Servers (cline_mcp_settings.json).
Restart the client after editing its config. A token in a config file is stored in plain text, so keep that file private. Signed in? Account → API has the same snippets with one-click install for Cursor and VS Code, and can fill in your token for you.
Tools
| Tool | Scope | What it does |
|---|---|---|
search_start | search | Run an email, username or phone search and wait up to 50 seconds for the result (wait, up to 120). Spends credits. |
search_get | read | Status of a search, with its findings once it is done |
search_list | read | Recent searches started through the API or MCP |
report_list | read | Your whole search history, newest first |
report_get | read | One report with all of its findings |
get_account | read | Credits left, the token's scope and limits |
With a read token, search_start is not offered to the assistant at all. Errors such as too few credits come back to the assistant as a readable message, so it can tell you what went wrong.
Test it without a client
The server speaks JSON-RPC over plain HTTP POST, so curl is enough to check that a token works:
curl -X POST {API base URL}/mcp \
-H "Authorization: Bearer $TRACEFIND_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'
Only search people you have a lawful reason to look into; that applies to an assistant acting for you as much as to you. See the legal and ethical guide.
Result format
results is a list of objects, each with a type. Which types appear depends on the search, and a type only appears when that check returned something:
| Search | Result types |
|---|---|
social (an account on a site, with name, url and any profile details in others), leaks (data breaches in data), google (public Google account details), email_verify (whether the mailbox exists), domain_validation (the domain's mail setup) | |
| Username | username (a profile, with sitename, url, tags and extracted profile data such as display name, bio or avatar) |
| Phone | lookup (validity, carrier, country, line type), whatsapp, telegram, signal, facebook, instagram (each with data.exists), temp (listings on receive-SMS sites), mentions (pages that mention the number) |
Forward compatible: new result types and new fields may be added to /api/v1 at any time. Ignore what you don't recognise; fields will not be removed or renamed within v1.
Errors
Errors use standard HTTP status codes and a JSON body with a human-readable detail string, safe to log or show:
HTTP/1.1 402 Payment Required
{"detail": "A search needs 5 purchased credits. Buy credits at https://tracefind.info/purchase.html"}
| Status | When | What to do |
|---|---|---|
400 | Unknown type, or the query is not a valid email, username or phone number. | Fix the input; don't retry. |
401 | No token, or it is wrong, revoked or expired, or the account was deleted. | Check the header; create a new token. |
402 | Fewer than 5 purchased credits, or a PDF of a free preview. | Buy credits. |
403 | A read-only token tried to start a search. | Use a token with the search scope. |
404 | No such search or report on your account. | Check the ID. |
422 | The request is malformed: missing field, wrong type, wait above 120, limit out of range. | Fix the request; detail is a list naming the field. |
429 | More than 60 requests in a minute, or 5 API searches already running. | Wait and retry (see below). |
5xx | Something went wrong on our side. | Retry with backoff; nothing was charged. |
For 422, detail is a list of problems instead of a string:
{"detail": [{"type": "less_than_equal", "loc": ["body", "wait"],
"msg": "Input should be less than or equal to 120", "input": 300}]}
Limits and credits
| Limit | Value | On exceeding it |
|---|---|---|
| Requests per token | 60 per minute (counted per calendar minute) | 429 with a Retry-After header (seconds until the next minute) |
| Running API searches per account | 5 queued or running at once | 429, no Retry-After: retry once one of your searches finishes |
| Active tokens per account | 10 | Revoke one on the dashboard |
wait on POST /searches | 120 seconds | 422 |
Polling counts towards the request limit: one poll every 5 seconds is 12 requests a minute per search. Use wait instead of polling where you can.
Credits
- A search costs 5 credits and is free when it finds nothing or fails, the same as on the site.
- API searches use purchased credits only, not the free signup credits. Buy credits; they don't expire.
- Reading searches, reports, PDFs and the account is free.
- If your credits run out while a search waits for a worker, it pauses as
waiting_for_creditsinstead of failing.
Acceptable use
Searches only query public sources and sign-up checks; the person you search is never contacted or notified. Use the API only for lawful purposes and with a legitimate reason, as set out in our terms of service. Results can contain personal data: store and share them according to the data protection law that applies to you (for example the GDPR).
Recipes
Retry on 429 with backoff (Python)
def call(method, path, **kw):
for attempt in range(5):
r = requests.request(method, f"{API}{path}", headers=HEADERS, timeout=130, **kw)
if r.status_code != 429 and r.status_code < 500:
break
time.sleep(int(r.headers.get("Retry-After", 2 ** attempt * 5)))
if not r.ok:
raise RuntimeError(f"{r.status_code}: {r.json().get('detail')}")
return r.json()
Search a list, at most 5 at a time (JavaScript)
const usernames = ['johndoe', 'jane_d', 'jd1990'];
const results = [];
for (let i = 0; i < usernames.length; i += 5) {
const batch = usernames.slice(i, i + 5).map((query) =>
fetch(`${API}/searches`, {
method: 'POST', headers,
body: JSON.stringify({ type: 'username', query, wait: 120 }),
}).then((r) => r.json()));
results.push(...(await Promise.all(batch)));
}
console.table(results.map((s) => ({ query: s.query, status: s.status, hits: s.summary?.hits })));
Export your whole history to JSON (Python)
reports, offset = [], 0
while True:
page = requests.get(f"{API}/reports", headers=HEADERS,
params={"limit": 50, "offset": offset, "results": "true"}).json()["reports"]
reports += page
if len(page) < 50:
break
offset += 50
json.dump(reports, open("tracefind-history.json", "w"), indent=2)
Running many searches from a spreadsheet? The bulk search page does the same without code.