TraceFind Blog Back to searchingSearch Start Now
  1. TraceFind
  2. Blog
  3. Tutorials
Tutorials

TraceFind API Tutorial: Run Searches and Fetch Reports from Code

Your CRM has a column of new signups and a support lead who keeps asking, "is this email real?" You could paste each one into a search box, or you could write forty lines that look every new address up and drop the summary into the ticket. This tutorial walks through the TraceFind API from an empty account to a script that runs searches, handles the awkward cases, and pulls PDFs, using only the endpoints and limits that exist today.

In short
  • The API runs the same email, username and phone searches as the site: JSON over HTTPS with a Bearer token.
  • One call starts a search. With "wait": 90 it usually returns the finished result; otherwise you poll every few seconds.
  • A search costs 5 purchased credits and is free when it finds nothing or fails. Reading history, reports and the account is free.
  • Limits: 60 requests a minute per token, 5 running searches per account, 10 active tokens.

Before the first request

You need three things: purchased credits (API searches never use the free sign-up credits), a token, and somewhere safe to keep it.

  1. Buy credits

    At least 5, enough for one search. Credits do not expire.

  2. Create a token

    Go to Account, then API. Name the token after the tool that will use it, pick a scope and copy the token. It is shown once, because only a SHA-256 hash is stored. Lost it? Revoke it and create a new one.

  3. Keep it out of your code

    Put it in an environment variable. Tokens start with tf_live_.

bash
# the API base URL is shown on the API docs page
export TRACEFIND_API="{API base URL}"
export TRACEFIND_TOKEN="tf_live_..."

There are two scopes. Search can do everything, including starting searches that spend credits. Read can do everything except that, which makes it the right choice for dashboards and backups. Use one token per tool (up to 10 active) so you can revoke one without touching the others.

Careful: Keep tokens on your server. Never put one in a web page, mobile app or public repository: anyone who holds it can spend your credits. The API never accepts or returns your account ID, so a leaked token cannot be used to take over the account, but it can still burn credits.

The endpoints-at-a-glance table from the TraceFind API docs: POST /api/v1/searches needs the search scope, and GET endpoints for searches, a single search, reports, a single report, its PDF and the account need the read scope.
The whole API on one screen. Only POST /api/v1/searches needs the search scope; everything else works with a read token.

Check that the token works

The account endpoint is free and tells you whether the token is valid, how many searches your credits cover, and the limits that apply.

bash
curl -s "$TRACEFIND_API/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}
}

A missing, wrong, revoked or expired token returns 401 with a WWW-Authenticate: Bearer header and a JSON body such as {"detail": "This API token is invalid or has been revoked."}.

An API search is queued, picked up by a worker within a second or two, and saved to your history as a report when it finishes. The flow below is the one every client follows, whatever the language.

Sequence diagram. Your script POSTs to /api/v1/searches and gets 201 with status queued. A worker picks it up in one to two seconds. The script polls GET /api/v1/searches/{id} every 5 seconds and gets queued or running. When the worker saves the report, the next poll returns done with a summary and results. The script then GETs /reports/{report_id}/pdf and receives a PDF file.
The lifecycle of one search. The polling loop disappears if you pass wait on the first request and the search finishes in time.

The request body has three fields:

FieldTypeNotes
typestring, requiredemail, username or phone (case-insensitive)
querystring, required1 to 320 characters. Phones in international format (+4915100000000; spaces and dashes are fine). Usernames: 2 to 64 characters of letters, digits, ., _, -
waitintegerSeconds (0 to 120) to hold the request until the search finishes. Default 0 returns at once with "status": "queued". Set your HTTP timeout a little higher than this.

Here is an email search that waits up to 90 seconds. Most searches finish well inside that: roughly 45 seconds for an email, 30 for a username and 15 for a phone number.

bash
curl -s -X POST "$TRACEFIND_API/api/v1/searches" \
  -H "Authorization: Bearer $TRACEFIND_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type": "email", "query": "jane.doe@example.com", "wait": 90}'

If you leave out wait, the response is 201 Created with a search object like this:

201 Created
{
  "id": "4CQL0DSL4KCC",
  "type": "phone",
  "query": "+4915100000000",
  "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
}

Poll for the result

If the search is still running after wait (or you did not use it), poll GET /api/v1/searches/{id} every 3 to 5 seconds until status is no longer queued or running. The statuses mean:

statusMeaning
queued, runningWaiting for a worker (usually a second or two), or searching
doneFinished. found says whether anything turned up; results holds it
failedDid not finish; see error. Nothing is charged; start it again
waiting_for_creditsCredits ran out before it could start; it continues once you buy credits
cancelledStopped before it ran

Here is the whole thing in Python, which handles the wait, the polling loop and a sensible reading of an email result:

python
# pip install requests
import os, time, requests

API = os.environ["TRACEFIND_API"] + "/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['TRACEFIND_TOKEN']}"}

def search(kind, query):
    r = requests.post(f"{API}/searches", headers=HEADERS, timeout=130,
                      json={"type": kind, "query": query, "wait": 90})
    r.raise_for_status()
    s = r.json()
    while s["status"] in ("queued", "running"):
        time.sleep(5)
        s = requests.get(f"{API}/searches/{s['id']}", headers=HEADERS, timeout=30).json()
    return s

s = search("email", "jane.doe@example.com")
print(s["status"], s["summary"])
for item in s.get("results", []):
    if item["type"] == "social":
        print("account:", item["name"], item["url"])
    elif item["type"] == "leaks":
        print("breaches:", [b.get("Name") for b in item["data"]])

The Node 18+ version is the same shape, using built-in fetch:

javascript
// Node 18+, as an ES module
const API = `${process.env.TRACEFIND_API}/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: 'username', query: 'jdoe-dev', wait: 90 }),
});
if (!res.ok) throw new Error((await res.json()).detail);
let s = await res.json();
while (['queued', 'running'].includes(s.status)) {
  await new Promise((r) => setTimeout(r, 5000));
  s = await (await fetch(`${API}/searches/${s.id}`, { headers })).json();
}
console.log(s.status, s.summary);

Reading the response

A finished search carries a summary with a few headline numbers, and a results list for the detail. The summary fields depend on the search type:

Searchsummary fields
emailhits, breaches, accounts, breach_names (up to 8), account_names (up to 8), deliverable (true, false or null)
usernamehits, profiles, sites (up to 12 names)
phonehits, messengers (for example ["WhatsApp", "Telegram"]), country, carrier

Each entry in results has a type, and a type only appears when that check returned something. For email you may see social (an account on a site, with name, url and details in others), leaks (breaches, in data), google, email_verify and domain_validation. A username search returns username entries (a profile with sitename, url, tags and extracted profile data). A phone search can return lookup, whatsapp, telegram, signal, facebook, instagram (each with data.exists), temp and mentions.

The API is forward compatible: new result types and fields may be added to /api/v1 at any time, but fields are not removed or renamed within v1. Ignore what you do not recognise rather than failing on it.

Two fields drive billing logic in your own code. found is false when nothing turned up, in which case the search was free. credits_spent is 5 when something was found and 0 otherwise.

Errors and limits

Errors use standard status codes and a JSON body with a human-readable detail string that is safe to log. The ones you will meet:

StatusWhenWhat to do
400Unknown type, or the query is not a valid email, username or phoneFix the input; do not retry
401No token, or it is wrong, revoked or expiredCheck the header; create a new token
402Fewer than 5 purchased credits, or a PDF of a free previewBuy credits
403A read-only token tried to start a searchUse a search-scope token
404No such search or report on your accountCheck the ID
422Malformed request, such as wait above 120; detail is a list naming the fieldFix the request
429More than 60 requests in a minute, or 5 API searches already runningWait and retry
5xxSomething went wrong on our sideRetry with backoff; nothing was charged
The Limits and credits section of the API docs. A table lists 60 requests per minute per token, 5 running API searches per account, 10 active tokens per account, and a wait of up to 120 seconds on POST /searches. Below it, notes say a search costs 5 credits and is free when it finds nothing, and that API searches use purchased credits only.
The limits as documented. Polling counts towards the request limit: one poll every 5 seconds is 12 requests a minute per search.

The two rate limits behave differently. The 60 requests a minute limit returns 429 with a Retry-After header holding the seconds until the next minute. The limit of 5 queued or running searches also returns 429 but with no Retry-After: retry once one of your searches has finished. Because polling uses your request budget, prefer wait over a tight loop wherever you can.

This retry helper treats both cases, and 5xx, the same way:

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()

Searching a list

With a limit of 5 running searches, a list of usernames needs batching. This sends five at a time and waits for each batch to finish:

javascript
const usernames = ['jdoe-dev', '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 })));

If you only need to run a spreadsheet through once and do not want to write code, the bulk search page does the same from a CSV.

Create a tokenAccount, then API: name it, choose a scope, copy it once. Read the full reference for every field.

Open the API docs

History, reports and PDFs

Every search you run, on the site or through the API, is saved as a report. Two endpoints cover it. GET /api/v1/searches lists your recent API searches (limit 1 to 100, default 20), without results. GET /api/v1/reports lists your whole history, newest first: filter by type, page with limit (1 to 50) and offset, and add results=true to include each report's results instead of a result_count.

A single report is at GET /api/v1/reports/{report_id}, and its PDF, the same document as the site's export, is at /reports/{report_id}/pdf. A report from a free trial on an account that has never bought credits comes back as a locked preview ("locked": true, no results), and its PDF returns 402.

python
r = requests.get(f"{API}/reports/{s['report_id']}/pdf", headers=HEADERS, timeout=60)
r.raise_for_status()
open("report.pdf", "wb").write(r.content)

# export your whole history
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

Habits that save credits and trouble

Do
  • Use wait instead of tight polling
  • Use one token per tool, with an expiry
  • Store report_id with your own record
  • Back off on 429 and 5xx
  • Have a lawful reason for each lookup
Don't
  • Put a token in client-side code
  • Fire hundreds of searches at once
  • Retry a 400 or 422 unchanged
  • Keep results longer than you need them
  • Search people you have no reason to look up

To see what the three searches return before you write code, read the email OSINT guide and the username OSINT guide. To keep watching a value rather than looking it up once, see breach monitoring and alerts.

Frequently asked questions

How do I authenticate with the TraceFind API?

Send an API token in the Authorization header as a Bearer token. Create tokens under Account, then API. A token is shown once, starts with tf_live_ and can have a search or read scope.

How much does an API search cost?

5 credits, and nothing when the search finds nothing or fails. API searches use purchased credits only, not the free sign-up credits. Reading searches, reports, PDFs and the account is free.

What are the rate limits?

60 requests per minute per token, 5 queued or running API searches per account and 10 active tokens per account. Exceeding the first two returns 429.

Should I poll or use the wait parameter?

Use wait where you can. It holds the request up to 120 seconds until the search finishes, and polling counts towards your 60 requests a minute. If the search is still running afterwards, poll every 3 to 5 seconds.

Can the API return the PDF report?

Yes. GET /api/v1/reports/{report_id}/pdf returns the same PDF as the site export. A free preview report returns 402 until credits have been purchased.

Will the person I search be notified?

No. Searches only query public sources and sign-up checks, and the person is never contacted or notified. You still need a lawful reason for each lookup.