Skip to content

API reference

Tracehound API

Run the same live username, email and phone searches as the app from your own code. One request in, one JSON document out, or a stream of events as each source answers.

v1 · Base URL https://tracehound-azure.vercel.app

OverviewLink to this section

The Tracehound API runs the same search as the app. You send a username, email address or phone number; Tracehound checks 430+ public sources live and returns every account that matched, with the public profile details each source returned. It is the same engine, the same sources and the same results as the app, in JSON.

Base URL
https://tracehound-azure.vercel.app
Version
v1
Endpoints
POST /api/v1/searchGET /api/v1/account
Authentication
API key in the Authorization header
Format
JSON in, JSON out (UTF-8). Streams are NDJSON.
Plans
Pro and Max

Paths are relative to the base URL. Every response carries Cache-Control: no-store; nothing is cached between requests, and every search asks the sources again.

AuthenticationLink to this section

Get a keyLink to this section

API access is part of the Pro and Max plans. Sign in, open Settings → API and create a key. Give it a name that says where it will run, such as the server or integration that uses it. Keys start with th_live_.

The full key is shown once, right after you create it. Copy it into your secret store then: Tracehound keeps only a SHA-256 digest of it, so nobody (including us) can show it to you again. Each account can have up to 5 active keys.

Send it with every requestLink to this section

Pass the key as a bearer token:

Request header
Authorization: Bearer YOUR_API_KEY

A missing, malformed, unknown or revoked key gets 401 unauthorized with a WWW-Authenticate: Bearer realm="api" header. A valid key on a plan without API access gets 403 plan_required.

Keep keys secretLink to this section

  • A key acts as your account. Searches made with it use your allowance, appear in your history if history is on, and are your responsibility under the Terms.
  • Use keys only on servers. Never put one in browser JavaScript, a mobile or desktop app, or a public repository. The API sends no CORS headers, so browsers block cross-origin calls to it anyway.
  • Load the key from an environment variable or a secret manager, not from source code.
  • Use one key per integration, so you can revoke one without breaking the others.

Revoke a keyLink to this section

Settings → API lists your active keys with their name, first characters, creation date and when each was last used. Revoking a key takes effect immediately: the next request made with it gets 401 unauthorized. If a key may have leaked, revoke it first, then create a replacement.

Quick startLink to this section

1. Check your key. GET /api/v1/account costs nothing and tells you your plan and how many searches you have left. Replace YOUR_API_KEY with your key.

Terminal
curl https://tracehound-azure.vercel.app/api/v1/account \
  -H "Authorization: Bearer YOUR_API_KEY"

2. Run a search. This looks up the username nightowl_dev and prints every account found. It uses one search from your allowance.

curl -X POST https://tracehound-azure.vercel.app/api/v1/search \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind": "username", "query": "nightowl_dev"}' \
  --max-time 120

The Node.js sample uses the built-in fetch (Node.js 18 or later); save it as search.mjs and run node search.mjs. The Python sample needs the requests package. The response is described under Response, with a full example.

POST/api/v1/search

Runs one search. By default the response arrives once the search has finished, as one JSON document. Email and phone searches usually take a few seconds. A username search checks several hundred sources and typically takes 10 to 30 seconds, so set your client timeout to at least 120 seconds (more for Deep Search). To receive matches as they arrive, use streaming.

Request bodyLink to this section

A JSON object, sent with Content-Type: application/json. Fields not listed here are ignored. The body can be at most 20,000 characters.

kindstringrequired

What query is: "username", "email" or "phone".

querystringrequired

The identifier to search for, up to 320 characters. See accepted identifiers.

countrystringoptional

Phone searches only. Two-letter ISO 3166-1 country code, such as "US" or "GB", used for numbers written without a leading + and country code. Not case-sensitive.

includeNsfwbooleanoptional

Include adult sites. When omitted, the preference saved in your account settings applies (off unless you turned it on).

deepobjectoptional

Deep Search (Max only): { "levels": 1-3, "follow": [...] }. See Deep Search.

streambooleanoptional

true returns newline-delimited JSON events as sources answer instead of one document at the end. See Streaming.

Accepted identifiersLink to this section

  • username
    Accepted
    2 to 64 characters: letters, digits, ., _ and -. It must start and end with a letter, digit or underscore, with no two dots in a row.
    Normalized to
    Leading @ and surrounding spaces removed. Case is kept.
  • email
    Accepted
    A standard email address, up to 254 characters.
    Normalized to
    Lower-cased.
  • phone
    Accepted
    A valid number with its country code (+1 415 555 0134), or in national format with country set. Spaces, dashes, dots and parentheses are allowed.
    Normalized to
    E.164, for example +14155550134.

Anything else is rejected with 400 invalid_query before a search is charged; the message says what is wrong.

ResponseLink to this section

200 OK with Content-Type: application/json, once the search has finished:

kindstring

The search type you sent.

querystring

The normalized identifier that was searched (see the table above).

statusstring

"completed". A search is recorded as "cancelled" when the client disconnects before anything was found, so you will not normally see that value.

foundinteger

Accounts found, across all Deep Search layers. Equal to the length of results.

checkedinteger

Source checks that ran, across all layers.

unansweredinteger

Checks where the source was blocked, timed out or gave no trustworthy answer. These are not counted as "not found": absence from the results is not proof that an account does not exist.

durationMsinteger

How long the search took, in milliseconds.

resultsResult[]

One entry per account found, in the order sources answered. See Result.

leadsLead[]

Further identifiers found in the results. See Lead.

layersLayer[]

The Deep Search layers that ran. Empty for a normal search. See Layer.

tokensobject

What this search cost. See Tokens.

ResultLink to this section

sourceobject

The site or service that reported the account.

source.idstring

Stable identifier, such as "github". Email sources start with email- and phone sources with phone-.

source.namestring

Display name, such as "GitHub".

source.categorystring

One of social, coding, gaming, video, music, art, images, blog, news, business, finance, shopping, tech, hobby, dating, health, political, messaging, identity, security, adult, misc.

source.homepagestring

The source's home page URL.

querystring

The identifier this account was found for. In Deep Search layers it differs from the root query.

queryKindstring

"username", "email" or "phone".

depthinteger

0 for the search you sent, 1 to 3 for Deep Search layers.

urlstring | null

Public URL of the account. null when the source confirms that an account exists but has no public profile page, which is the case for most email checks.

profileProfile | null

Public profile details the source returned, or null when it returned none. See Profile.

Every phone search also returns a result from the phone-number-info source, with the number's country, line type and formats in profile.extra.

ProfileLink to this section

Every field is optional. A source sets only what it publishes, and Tracehound never infers or fills in values. A field the source did not provide is left out, not set to null.

displayNamestring

Name shown on the profile.

handlestring

The handle as the platform displays it. Its case can differ from your query.

avatarUrlstring

Profile picture URL, hosted by the source.

biostring

Profile description, as plain text.

followersinteger

Follower count.

followinginteger

Number of accounts followed.

postsinteger

Posts, repositories, videos or uploads, whichever the platform counts.

verifiedboolean

The platform's own verified mark.

privateboolean

The account is private or protected on the platform.

locationstring

Location as written on the profile (free text).

websitestring

Website listed on the profile.

emailstring

Email address shown publicly on the profile.

joinedAtstring

When the account was created: an ISO 8601 date or date-time.

lastActiveAtstring

Last activity the source reports: an ISO 8601 date or date-time.

links{ label, url }[]

Other accounts and links the profile lists publicly. Both fields are strings.

extraobject

Platform-specific facts as label and value pairs (string, number or boolean), such as { "Blitz rating": 1488 }. Labels are written for people and can change; do not build strict parsers on them.

LeadLink to this section

Leads are other identifiers that matched profiles list publicly, such as an email address on a developer profile. They are what Deep Search follows.

kindstring

"username", "email", "phone", "name" or "url".

valuestring

The identifier, normalized the same way as queries. URLs are absolute.

notestringoptional

Where it came from, for people: "Public email on GitHub profile".

fromstring

The source.id that reported it, or "email" for the username taken from the local part of a searched email address.

depthinteger

The layer it was found in.

LayerLink to this section

depthinteger

Layer number, from 1.

queries{ kind, value }[]

The identifiers this layer searched.

TokensLink to this section

chargedinteger

Searches charged when the search started: 1, plus one search per Deep Search level.

refundedinteger

Searches returned automatically, for Deep Search layers that did not run.

remaininginteger

Searches left on the account after this one, monthly allowance and bonus searches together.

Example responseLink to this section

200 OK · application/json
{
  "kind": "username",
  "query": "nightowl_dev",
  "status": "completed",
  "found": 16,
  "checked": 422,
  "unanswered": 31,
  "durationMs": 14820,
  "results": [
    {
      "source": {
        "id": "github",
        "name": "GitHub",
        "category": "coding",
        "homepage": "https://github.com"
      },
      "query": "nightowl_dev",
      "queryKind": "username",
      "depth": 0,
      "url": "https://github.com/nightowl_dev",
      "profile": {
        "displayName": "Mara Quill",
        "handle": "nightowl_dev",
        "avatarUrl": "https://avatars.example.com/u/4815162",
        "bio": "Backend developer. Night shifts, strong coffee.",
        "followers": 312,
        "following": 48,
        "posts": 37,
        "location": "Lisbon",
        "website": "https://nightowl.example.net/",
        "email": "mara.q@example.net",
        "joinedAt": "2017-03-14T09:21:07.000Z",
        "links": [
          { "label": "Mastodon", "url": "https://mastodon.example.net/@nightowl" }
        ]
      }
    },
    {
      "source": {
        "id": "chess-com",
        "name": "Chess.com",
        "category": "gaming",
        "homepage": "https://www.chess.com"
      },
      "query": "nightowl_dev",
      "queryKind": "username",
      "depth": 0,
      "url": "https://www.chess.com/member/nightowl_dev",
      "profile": {
        "handle": "nightowl_dev",
        "followers": 19,
        "joinedAt": "2020-11-02T18:40:12.000Z",
        "lastActiveAt": "2026-11-08T22:14:51.000Z",
        "extra": { "Country": "PT", "Blitz rating": 1488 }
      }
    },
    {
      "source": {
        "id": "wmn-buy-me-a-coffee",
        "name": "Buy Me a Coffee",
        "category": "finance",
        "homepage": "https://buymeacoffee.com"
      },
      "query": "nightowl_dev",
      "queryKind": "username",
      "depth": 0,
      "url": "https://buymeacoffee.com/nightowl_dev",
      "profile": null
    }
  ],
  "leads": [
    {
      "kind": "email",
      "value": "mara.q@example.net",
      "note": "Public email on GitHub profile",
      "from": "github",
      "depth": 0
    },
    {
      "kind": "url",
      "value": "https://nightowl.example.net/",
      "note": "Website on GitHub profile",
      "from": "github",
      "depth": 0
    }
  ],
  "layers": [],
  "tokens": { "charged": 1, "refunded": 0, "remaining": 499 }
}
Invented handle and person: the profile and every example.com / example.net address are fictional. Trimmed to three of sixteen results.

StreamingLink to this section

Add "stream": true and the response is 200 OK with Content-Type: application/x-ndjson: one JSON event per line, sent as sources answer. Use it to show matches while the search runs, or to act on the first match without waiting for the slowest source.

Terminal
curl -N -X POST https://tracehound-azure.vercel.app/api/v1/search \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind": "email", "query": "mara.q@example.net", "stream": true}'
200 OK · application/x-ndjson
{"t":"start","kind":"email","query":"mara.q@example.net","total":15,"tier":"pro","tokensCharged":1,"tokensRemaining":498}
{"t":"lead","lead":{"kind":"username","value":"mara.q","note":"Local part of the email address"},"from":"email","depth":0}
{"t":"tick","id":"email-spotify","s":"n","depth":0}
{"t":"result","item":{"moduleId":"email-gravatar","name":"Gravatar","category":"identity","homepage":"https://gravatar.com","query":"mara.q@example.net","queryKind":"email","depth":0,"url":"https://gravatar.com/maraquill","profile":{"displayName":"Mara Quill"}}}
{"t":"tick","id":"email-chess","s":"u","depth":0}
{"t":"done","found":1,"checked":15,"unknown":1,"ms":1694,"refunded":0,"locked":0,"tokensRemaining":498}
Shortened: a real stream has one tick per source checked. The Gravatar match is invented.

Event referenceLink to this section

Every event has a t field with its type. start always comes first and done always comes last; in between, events arrive in the order sources answer. Ignore event types you do not recognize.

startevent

kind, query (normalized), total (sources that will be checked in the first layer), tier (your plan), tokensCharged, tokensRemaining.

resultevent

An account was found: item with moduleId, name, category, homepage, query, queryKind, depth, url and profile.

This is the format the app uses, so it differs slightly from the JSON response: the source fields are not nested under source (moduleId is source.id), and url and profile are left out rather than null when there is none. The format also defines locked and lockedFields for the demo and the Free plan; they never occur on plans with API access.

tickevent

A source finished without a match: id (the source id), s ("n" not found, "u" no trustworthy answer), depth. Useful for progress bars.

leadevent

lead (kind, value, optional note), from and depth, as in Lead.

layerevent

A Deep Search layer starts: depth, queries (kind and value of each identifier followed) and total (sources this layer will check).

errorevent

The search failed: code (no_answers or server_error) and message. It is not charged; the refund shows in done.

leads_lockedevent

count of leads hidden by the plan. Part of the shared format, but it never occurs on plans with API access.

doneevent

found, checked, unknown (same as unanswered), ms, refunded, locked (always 0 on plans with API access) and tokensRemaining. The stream closes after it.

Read a stream in Node.jsLink to this section

Split the body on newlines and parse each complete line. A network chunk can end in the middle of a line, so keep the remainder for the next chunk:

stream.mjs
// stream.mjs (Node.js 18 or later)
const API_KEY = "YOUR_API_KEY";

const response = await fetch("https://tracehound-azure.vercel.app/api/v1/search", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ kind: "username", query: "nightowl_dev", stream: true }),
  signal: AbortSignal.timeout(120_000),
});
if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${response.status} ${error.code}: ${error.message}`);
}

// One JSON event per line. A chunk can end mid-line, so keep the remainder for the next one.
let buffer = "";
for await (const chunk of response.body.pipeThrough(new TextDecoderStream())) {
  buffer += chunk;
  let newline;
  while ((newline = buffer.indexOf("\n")) !== -1) {
    const line = buffer.slice(0, newline);
    buffer = buffer.slice(newline + 1);
    if (line.trim()) handle(JSON.parse(line));
  }
}

function handle(event) {
  switch (event.t) {
    case "start":
      console.log(`Checking ${event.total} sources for ${event.query}`);
      break;
    case "result":
      console.log("Found:", event.item.name, event.item.url ?? "");
      break;
    case "lead":
      console.log("Lead:", event.lead.kind, event.lead.value);
      break;
    case "error":
      console.error(`Search failed (${event.code}): ${event.message}`);
      break;
    case "done":
      console.log(`Done: ${event.found} found, ${event.checked} checked in ${event.ms} ms`);
      break;
  }
}

If you close the connection early, the search stops. It is refunded if nothing had been found yet; otherwise it counts as a search.

Deep Search (Max plan) follows the trail for you. After the first search, up to 5 new usernames or email addresses found as leads are searched as the next layer, for up to 3 layers. Add a deep object to the request:

deep.levelsintegerrequired

Layers to follow, 1 to 3. 0 is the same as leaving deep out.

deep.followstring[]optional

Which lead kinds to follow: "username", "email" or both. Default: both.

Terminal
curl -X POST https://tracehound-azure.vercel.app/api/v1/search \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind": "email", "query": "mara.q@example.net", "deep": {"levels": 2, "follow": ["username"]}}' \
  --max-time 300
  • It costs 1 search plus one search per level, charged when the search starts. Layers that never run because there was nothing new to follow are refunded automatically, and tokens.refunded says how many.
  • Results from later layers have depth 1 or more, and their query is the identifier that was followed. layers lists what each layer searched.
  • Identifiers whose owners have opted out are skipped.
  • Allow up to 300 seconds for a Deep Search to finish.
  • On a plan without Deep Search, a request with deep.levels of 1 or more gets 403 plan_required and is not charged.

In the example above, the address's local part mara.q was followed as a username in layer 1 and layer 2 had nothing new to follow, so the response ended with:

Excerpt
"layers": [{ "depth": 1, "queries": [{ "kind": "username", "value": "mara.q" }] }],
"tokens": { "charged": 3, "refunded": 1, "remaining": 2498 }

AccountLink to this section

GET/api/v1/account

Returns the plan and remaining searches of the account that owns the key. It costs nothing and does not count toward the rate limit, so it is a good way to test a key or to check your allowance before a batch.

Terminal
curl https://tracehound-azure.vercel.app/api/v1/account \
  -H "Authorization: Bearer YOUR_API_KEY"
200 OK · application/json
{
  "plan": "pro",
  "searches": {
    "perMonth": 500,
    "used": 41,
    "bonus": 10,
    "remaining": 469,
    "resetsAt": "2026-12-10T04:06:29.891Z"
  },
  "rateLimit": { "requestsPerMinute": 30 }
}
planstring

"pro" or "max".

searches.perMonthinteger

Searches included in the plan each month.

searches.usedinteger

Monthly searches used in the current period.

searches.bonusinteger

Bonus searches from referrals. They never expire and are used after the monthly allowance.

searches.remaininginteger

Searches you can still run: monthly searches left plus bonus searches.

searches.resetsAtstring

ISO 8601 date-time when the monthly allowance resets.

rateLimit.requestsPerMinuteinteger

Search requests allowed per minute for each key. See Rate limits.

ErrorsLink to this section

Every endpoint uses the same error format, with a non-2xx status:

400 Bad Request · application/json
{
  "error": {
    "code": "invalid_query",
    "message": "Usernames can be 2 to 64 characters: letters, numbers, dots, dashes and underscores."
  }
}

Branch on the HTTP status and code; both are stable. The message is written for people and may change. Treat a code you do not recognize according to its status.

  • 400invalid_request
    Meaning
    The body is not valid JSON, a required field is missing, or a field has the wrong type or value.
    Charged
    No
    What to do
    Fix the request. The message names the problem.
  • 400invalid_query
    Meaning
    The identifier is not in an accepted format.
    Charged
    No
    What to do
    Check accepted identifiers. For phone numbers, add the country code or set country.
  • 401unauthorized
    Meaning
    The key is missing, malformed, unknown or revoked.
    Charged
    No
    What to do
    Check the Authorization header. Create a new key if this one was revoked.
  • 402out_of_tokens
    Meaning
    Not enough searches left for this request, including Deep Search levels.
    Charged
    No
    What to do
    Wait for resetsAt, lower deep.levels, or upgrade.
  • 403plan_required
    Meaning
    The plan has no API access, or Deep Search was requested on a plan without it.
    Charged
    No
    What to do
    Upgrade, or leave out deep.
  • 403opted_out
    Meaning
    The owner of this identifier has opted out of searches.
    Charged
    No
    What to do
    Nothing. Do not retry or work around it. See Opt-outs.
  • 403email_unverified
    Meaning
    The account's email address has not been confirmed.
    Charged
    No
    What to do
    Sign in to the app and confirm your email address.
  • 413invalid_request
    Meaning
    The body is larger than 20,000 characters.
    Charged
    No
    What to do
    Send only the documented fields.
  • 429rate_limited
    Meaning
    Too many requests in the current minute.
    Charged
    No
    What to do
    Wait the number of seconds in Retry-After, then retry.
  • 500server_error
    Meaning
    Something failed on our side.
    Charged
    No
    What to do
    Retry with backoff.
  • 502no_answers
    Meaning
    No source could be reached.
    Charged
    No
    What to do
    Retry later with backoff.

In a stream, no_answers and server_error arrive as an error event instead, with the same meaning.

Rate limitsLink to this section

  • Free
    API access
    No
    Requests per minute
    –
    Active keys
    –
    Searches per month
    25
  • Pro
    API access
    Yes
    Requests per minute
    30
    Active keys
    Up to 5
    Searches per month
    500
  • Max
    API access
    Yes
    Requests per minute
    60
    Active keys
    Up to 5
    Searches per month
    2,500
  • The limit counts POST /api/v1/search requests for each key, in fixed one-minute windows that start on the minute. GET /api/v1/account is not counted.
  • Requests rejected as invalid_request or invalid_query are not counted. Every other search request is, including ones rejected because of the plan, an opt-out or the allowance.
  • Over the limit, you get 429 rate_limited with a Retry-After header: the number of seconds until the window resets.
  • To slow down key guessing, a client address that sends more than 30 requests with invalid keys in a minute gets 429 for the rest of that minute, whatever key it sends.

Searches and billingLink to this section

  • One allowance. Every API search uses one search from the same monthly allowance as the app. There is no separate API price or quota. Plans are on the pricing page.
  • Deep Search adds one search per level.
  • Charged up front, refunded automatically. Searches are charged when they start. A search that fails (500, 502 or an error event) is refunded in full, Deep Search layers that never run are refunded, and so is a search you cancel by disconnecting before anything was found.
  • Rejected requests are free. Nothing is charged for any 4xx response, and GET /api/v1/account is always free.
  • Every response says what it cost in the tokens object: charged, refunded and remaining.
  • No overage charges. When the allowance is used up, searches return 402 out_of_tokens until it resets or you upgrade. Unused monthly searches do not roll over; referral bonus searches never expire.

Timeouts and retriesLink to this section

  • Timeouts. Allow at least 120 seconds for a search and 300 seconds for Deep Search. Most finish much sooner. If your client gives up first, the search is cancelled: refunded if nothing had been found yet, counted otherwise.
  • Retry 429 after the number of seconds in Retry-After.
  • Retry 502 no_answers and other 5xx with exponential backoff and a little random jitter, for example after about 2, 4 and 8 seconds, then stop and report the failure. These were not charged.
  • Do not blindly retry other 4xx responses. The same request will fail the same way. Fix the request, wait for the allowance, or, for opted_out, stop.
  • Every accepted request is a new search. There are no idempotency keys, so a retry after a timeout or dropped connection runs, and charges, a new search.
  • Pace batches. Run a few searches at a time and stay under your rate limit, rather than firing a whole list at once.

Opt-outsLink to this section

Anyone can stop searches for their own identifiers on the opt-out page, with or without an account. Opt-outs apply to the API exactly as to the app, and take effect immediately, so an identifier that could be searched yesterday may return 403 opted_out today. Nothing is charged.

What to do: nothing. Record that the identifier cannot be searched and move on. Do not retry it, search variations of it, or use other means to reach the same person; the Terms forbid working around an opt-out. In Deep Search, opted-out identifiers found as leads are skipped.

Responsible useLink to this section

The Acceptable Use Policy applies to the API exactly as it does to the app. In short:

  • A match is a research lead, not proof of identity. Verify before you act on it, and make that clear to anyone who sees results through your product.
  • Tracehound is not a consumer reporting agency. Do not use results for decisions about employment, housing, credit or insurance.
  • Never use it to stalk, harass or locate anyone, or to build or sell databases of people.
  • Do not resell or sublicense API access. The API terms have the details.

Keys used in breach of the policy may be suspended.

Versioning and changesLink to this section

The version is part of the path: /api/v1/. Within v1 we may add things without notice, so build clients that tolerate them:

  • new fields in responses and events, and new optional request fields;
  • new event types in streams;
  • new error codes (handle unknown ones by their HTTP status);
  • new sources, source ids and categories, as sites come and go.

Changes that would break a working client, such as removing or renaming a field or changing its type or meaning, will come as a new version path. We will tell account holders with API keys by email before v1 changes in a breaking way.

SupportLink to this section

Email support@example.com or use the contact form. Include the endpoint, the time of the request, the HTTP status and the error code. Never send your API key; the first characters shown in settings are enough to identify it.