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
Authorizationheader - 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:
Authorization: Bearer YOUR_API_KEYA 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.
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// search.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" }),
// A username search usually takes 10 to 30 seconds. Allow more for Deep Search.
signal: AbortSignal.timeout(120_000),
});
const data = await response.json();
if (!response.ok) {
throw new Error(`${response.status} ${data.error.code}: ${data.error.message}`);
}
console.log(`${data.found} accounts found, ${data.tokens.remaining} searches left`);
for (const result of data.results) {
console.log(result.source.name, result.url ?? "(no public profile URL)");
}# search.py (pip install requests)
import requests
API_KEY = "YOUR_API_KEY"
response = requests.post(
"https://tracehound-azure.vercel.app/api/v1/search",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"kind": "username", "query": "nightowl_dev"},
# A username search usually takes 10 to 30 seconds. Allow more for Deep Search.
timeout=120,
)
data = response.json()
if not response.ok:
raise RuntimeError(f"{response.status_code} {data['error']['code']}: {data['error']['message']}")
print(f"{data['found']} accounts found, {data['tokens']['remaining']} searches left")
for result in data["results"]:
print(result["source"]["name"], result["url"] or "(no public profile URL)")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.
Run a searchLink to this section
/api/v1/searchRuns 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.
kindstringrequiredWhat
queryis:"username","email"or"phone".querystringrequiredThe identifier to search for, up to 320 characters. See accepted identifiers.
countrystringoptionalPhone 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.includeNsfwbooleanoptionalInclude adult sites. When omitted, the preference saved in your account settings applies (off unless you turned it on).
deepobjectoptionalDeep Search (Max only):
{ "levels": 1-3, "follow": [...] }. See Deep Search.streambooleanoptionaltruereturns newline-delimited JSON events as sources answer instead of one document at the end. See Streaming.
Accepted identifiersLink to this section
| Kind | Accepted | Normalized to |
|---|---|---|
username | 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. | Leading @ and surrounding spaces removed. Case is kept. |
email | A standard email address, up to 254 characters. | Lower-cased. |
phone | 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. | E.164, for example +14155550134. |
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 withcountryset. 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:
kindstringThe search type you sent.
querystringThe 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.foundintegerAccounts found, across all Deep Search layers. Equal to the length of
results.checkedintegerSource checks that ran, across all layers.
unansweredintegerChecks 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.
durationMsintegerHow 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.
tokensobjectWhat this search cost. See Tokens.
ResultLink to this section
sourceobjectThe site or service that reported the account.
source.idstringStable identifier, such as
"github". Email sources start withemail-and phone sources withphone-.source.namestringDisplay name, such as
"GitHub".source.categorystringOne of
social,coding,gaming,video,music,art,images,blog,news,business,finance,shopping,tech,hobby,dating,health,political,messaging,identity,security,adult,misc.source.homepagestringThe source's home page URL.
querystringThe identifier this account was found for. In Deep Search layers it differs from the root query.
queryKindstring"username","email"or"phone".depthinteger0for the search you sent, 1 to 3 for Deep Search layers.urlstring | nullPublic URL of the account.
nullwhen the source confirms that an account exists but has no public profile page, which is the case for most email checks.profileProfile | nullPublic profile details the source returned, or
nullwhen 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.
displayNamestringName shown on the profile.
handlestringThe handle as the platform displays it. Its case can differ from your query.
avatarUrlstringProfile picture URL, hosted by the source.
biostringProfile description, as plain text.
followersintegerFollower count.
followingintegerNumber of accounts followed.
postsintegerPosts, repositories, videos or uploads, whichever the platform counts.
verifiedbooleanThe platform's own verified mark.
privatebooleanThe account is private or protected on the platform.
locationstringLocation as written on the profile (free text).
websitestringWebsite listed on the profile.
emailstringEmail address shown publicly on the profile.
joinedAtstringWhen the account was created: an ISO 8601 date or date-time.
lastActiveAtstringLast 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.
extraobjectPlatform-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".valuestringThe identifier, normalized the same way as queries. URLs are absolute.
notestringoptionalWhere it came from, for people:
"Public email on GitHub profile".fromstringThe
source.idthat reported it, or"email"for the username taken from the local part of a searched email address.depthintegerThe layer it was found in.
LayerLink to this section
depthintegerLayer number, from 1.
queries{ kind, value }[]The identifiers this layer searched.
TokensLink to this section
chargedintegerSearches charged when the search started: 1, plus one search per Deep Search level.
refundedintegerSearches returned automatically, for Deep Search layers that did not run.
remainingintegerSearches left on the account after this one, monthly allowance and bonus searches together.
Example responseLink to this section
{
"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 }
}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.
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}'{"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}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.
starteventkind,query(normalized),total(sources that will be checked in the first layer),tier(your plan),tokensCharged,tokensRemaining.resulteventAn account was found:
itemwithmoduleId,name,category,homepage,query,queryKind,depth,urlandprofile.This is the format the app uses, so it differs slightly from the JSON response: the source fields are not nested under
source(moduleIdissource.id), andurlandprofileare left out rather thannullwhen there is none. The format also defineslockedandlockedFieldsfor the demo and the Free plan; they never occur on plans with API access.tickeventA source finished without a match:
id(the source id),s("n"not found,"u"no trustworthy answer),depth. Useful for progress bars.leadeventlead(kind,value, optionalnote),fromanddepth, as in Lead.layereventA Deep Search layer starts:
depth,queries(kindandvalueof each identifier followed) andtotal(sources this layer will check).erroreventThe search failed:
code(no_answersorserver_error) andmessage. It is not charged; the refund shows indone.leads_lockedeventcountof leads hidden by the plan. Part of the shared format, but it never occurs on plans with API access.doneeventfound,checked,unknown(same asunanswered),ms,refunded,locked(always 0 on plans with API access) andtokensRemaining. 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 (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 SearchLink to this section
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.levelsintegerrequiredLayers to follow, 1 to 3.
0is the same as leavingdeepout.deep.followstring[]optionalWhich lead kinds to follow:
"username","email"or both. Default: both.
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.refundedsays how many. - Results from later layers have
depth1 or more, and theirqueryis the identifier that was followed.layerslists 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.levelsof 1 or more gets403 plan_requiredand 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:
"layers": [{ "depth": 1, "queries": [{ "kind": "username", "value": "mara.q" }] }],
"tokens": { "charged": 3, "refunded": 1, "remaining": 2498 }AccountLink to this section
/api/v1/accountReturns 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.
curl https://tracehound-azure.vercel.app/api/v1/account \
-H "Authorization: Bearer YOUR_API_KEY"{
"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.perMonthintegerSearches included in the plan each month.
searches.usedintegerMonthly searches used in the current period.
searches.bonusintegerBonus searches from referrals. They never expire and are used after the monthly allowance.
searches.remainingintegerSearches you can still run: monthly searches left plus bonus searches.
searches.resetsAtstringISO 8601 date-time when the monthly allowance resets.
rateLimit.requestsPerMinuteintegerSearch 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:
{
"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.
| Status and code | Meaning | Charged | What to do |
|---|---|---|---|
400invalid_request | The body is not valid JSON, a required field is missing, or a field has the wrong type or value. | No | Fix the request. The message names the problem. |
400invalid_query | The identifier is not in an accepted format. | No | Check accepted identifiers. For phone numbers, add the country code or set country. |
401unauthorized | The key is missing, malformed, unknown or revoked. | No | Check the Authorization header. Create a new key if this one was revoked. |
402out_of_tokens | Not enough searches left for this request, including Deep Search levels. | No | Wait for resetsAt, lower deep.levels, or upgrade. |
403plan_required | The plan has no API access, or Deep Search was requested on a plan without it. | No | Upgrade, or leave out deep. |
403opted_out | The owner of this identifier has opted out of searches. | No | Nothing. Do not retry or work around it. See Opt-outs. |
403email_unverified | The account's email address has not been confirmed. | No | Sign in to the app and confirm your email address. |
413invalid_request | The body is larger than 20,000 characters. | No | Send only the documented fields. |
429rate_limited | Too many requests in the current minute. | No | Wait the number of seconds in Retry-After, then retry. |
500server_error | Something failed on our side. | No | Retry with backoff. |
502no_answers | No source could be reached. | No | Retry later with backoff. |
- 400
invalid_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.
- 400
invalid_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.
- 401
unauthorized- Meaning
- The key is missing, malformed, unknown or revoked.
- Charged
- No
- What to do
- Check the
Authorizationheader. Create a new key if this one was revoked.
- 402
out_of_tokens- Meaning
- Not enough searches left for this request, including Deep Search levels.
- Charged
- No
- What to do
- Wait for
resetsAt, lowerdeep.levels, or upgrade.
- 403
plan_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.
- 403
opted_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.
- 403
email_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.
- 413
invalid_request- Meaning
- The body is larger than 20,000 characters.
- Charged
- No
- What to do
- Send only the documented fields.
- 429
rate_limited- Meaning
- Too many requests in the current minute.
- Charged
- No
- What to do
- Wait the number of seconds in
Retry-After, then retry.
- 500
server_error- Meaning
- Something failed on our side.
- Charged
- No
- What to do
- Retry with backoff.
- 502
no_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
| Plan | API access | Requests per minute | Active keys | Searches per month |
|---|---|---|---|---|
| Free | No | – | – | 25 |
| Pro | Yes | 30 | Up to 5 | 500 |
| Max | Yes | 60 | Up to 5 | 2,500 |
- 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/searchrequests for each key, in fixed one-minute windows that start on the minute.GET /api/v1/accountis not counted. - Requests rejected as
invalid_requestorinvalid_queryare 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_limitedwith aRetry-Afterheader: 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
429for 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,502or anerrorevent) 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/accountis 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_tokensuntil 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
429after the number of seconds inRetry-After. - Retry
502 no_answersand 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.