Running a lookup

Send an email, a phone number, a username (or query, auto-detected), optionally an IP address, and the modules you want. One request, one answer: the footprint, each module's facts and risk, and an overall risk score.

Several identities, one request

v2 takes email and phone together: each gets its own footprint sweep, and every row in footprint.results says which one it came from in identity (email, phone or username). Two identities are two footprint units, and meta.modules lists exactly what ran - that is what you are billed for. An IP address is never swept; it feeds the IP module.

POST /v2/lookup?wait=true
curl -X POST "https://api.digifootprint.dev/v2/lookup?wait=true" \
  -H "Authorization: Bearer dfp_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane.doe@gmail.com",
    "phone": "+14155552671",
    "ip": "45.83.91.1",
    "checks": { "email": true, "phone": true, "ip": true, "riskSignals": true }
  }'

Don't know what the input is? Send query

query takes anything - an email, a phone number, an IP address or a username - and puts it where it belongs: an email becomes email, a phone number phone, an IP ip. The request then runs exactly as if you had sent that field, so it combines with the others. A username stays in query. If query lands on a field you also sent with a different value, the answer is a 422 query_conflict rather than a guess.

POST /v2/lookup
{
  "query": "45.83.91.1",
  "email": "jane.doe@gmail.com",
  "checks": { "email": true, "ip": true }
}
// runs exactly as { "email": "jane.doe@gmail.com", "ip": "45.83.91.1", ... }

Footprint only: POST /v2/footprint

Just the accounts behind an email, phone or username - the same sweep, breaches and web mentions as /v2/lookup, with every other module off. Send query, email, phone, or an email and a phone together (two sweeps). Billed as footprint lookups only.

POST /v2/footprint
curl -X POST https://api.digifootprint.dev/v2/footprint \
  -H "Authorization: Bearer dfp_your_key" \
  -H "Content-Type: application/json" \
  -d '{"query": "jane.doe@gmail.com"}'

# 200 OK
{
  "lookupId": "8f2c1d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
  "footprint": { "platformsChecked": 1944, "matches": 6, "hits": ["instagram", "github"], "results": [...] }
}

Synchronous

?wait=true blocks until every platform has answered. Simplest to integrate; expect several seconds.

POST /v2/lookup?wait=true
curl -X POST "https://api.digifootprint.dev/v2/lookup?wait=true" \
  -H "Authorization: Bearer dfp_your_key" \
  -H "Content-Type: application/json" \
  -d '{"query": "someone@example.com"}'

Early answer: act on the first profiles

A full sweep checks thousands of platforms and the slowest sites decide when it ends. If you are sitting in front of a sign-up or a payment, you usually need some evidence now, not all of it. Add early to the footprint check and the call answers as soon as each identity has its first real profiles and every module you asked for has answered - typically in one to three seconds.

POST /v2/lookup?wait=true
{
  "email": "someone@example.com",
  "ip": "8.8.8.8",
  "checks": {
    "footprint": { "early": true, "hits": 3, "maxWaitMs": 2500 },
    "email": true,
    "ip": true
  }
}

hits (1-10, default 3) is how many real profiles to wait for - a number, not the same thing as the response's footprint.hits list of platforms; maxWaitMs (500-15000, default 2500) is a hard ceiling - the answer goes out then with whatever is in. The response has status: "processing" and meta.footprintComplete: false; rules that need the whole footprint (such as “no accounts anywhere”) do not fire yet.

The sweep keeps going. The same lookup is completed in the background: GET /v2/lookup/:id returns everything found so far, then status: "completed" with the full footprint and a re-scored risk. The lookup.completed webhook fires once, when it is complete. Without early, nothing changes. Every key and option of checks is on checks.

Queued

Without ?wait=true you get a lookupId straight away and poll for the result. For a responsive UI, prefer streaming over polling.

POST /v2/lookup
# Returns immediately with an id to poll
curl -X POST "https://api.digifootprint.dev/v2/lookup" \
  -H "Authorization: Bearer dfp_your_key" \
  -H "Content-Type: application/json" \
  -d '{"query": "someone@example.com"}'

# {"lookupId": "8f2c...", "status": "processing"}

curl "https://api.digifootprint.dev/v2/lookup/8f2c..." \
  -H "Authorization: Bearer dfp_your_key"

Depth: instant, standard and deep

Three depths. Pick one with depth in the request body - "instant", "standard" or "deep" - or with the query parameters ?instant=true / ?deep=true. The default is standard. When both are sent, the body wins. Any other depth value is rejected with a 400 rather than quietly running a standard lookup.

Standard (no flag) checks the default band of platforms and is what the queued and ?wait=true paths above run.

?instant=true is latency-bounded: it runs a high-signal subset against a wall clock (900ms by default) and returns whatever answered in time. It is always synchronous - the point is an answer right now, so it implies ?wait=true and you never need to send it. Reach for it when you are fanning out to several APIs inside one transaction and cannot absorb a slow one.

?deep=true checks the full platform set including the long tail. Slower and unbounded - use it when coverage matters more than latency, and pair it with streaming so results surface as they land. The queued path (POST without wait) already runs at this depth.

POST /v2/lookup?instant=true
# Always synchronous - ?instant=true implies ?wait=true
curl -X POST "https://api.digifootprint.dev/v2/lookup?instant=true" \
  -H "Authorization: Bearer dfp_your_key" \
  -H "Content-Type: application/json" \
  -d '{"query": "someone@example.com"}'

Reading an instant response

Every v2 response carries meta; on an instant lookup its deadline fields are filled in. Instant takes one identity per request - sending email and phone together with ?instant=true is a 422.

200 OK - ?instant=true
{
  "lookupId": "3b1f...",
  "input": { "email": "someone@example.com", "phone": null, "username": null, "ip": null },
  "risk": null,
  "footprint": {
    "platformsChecked": 25,
    "matches": 2,
    "socialMatches": 2,
    "derivedMatches": 0,
    "hits": ["github", "spotify"],
    "servedFromCache": false,
    "results": [
      { "platform": "github",  "registered": true, "method": "public_api", "checkedAt": "2026-09-25T09:12:04.377Z", "identity": "email" },
      { "platform": "spotify", "registered": true, "method": "public_api", "checkedAt": "2026-09-25T09:12:04.201Z", "identity": "email" }
    ],
    "breaches": [],
    "webMentions": null
  },
  "email": null,
  "phone": null,
  "ip": null,
  "meta": { "tier": "instant", "modules": ["footprint:email"], "instant": true, "deadlineMs": 900, "platformsPending": 3 }
}

meta.instant: true marks the response as bounded. meta.deadlineMs echoes the budget it was held to, so you can tell which ceiling produced this answer. meta.platformsPending is the one to branch on: it counts platforms that had not answered when the deadline hit.

A non-zero meta.platformsPending means fast, not complete. Those platforms were not checked and found absent - they simply ran out of time, and a standard lookup would likely return more. This is the same rule as everywhere else here: a platform missing from footprint.results was never confidently answered, and instant mode only adds “we ran out of time” to the list of reasons why. Nothing is guessed to fill the gap. See instant vs deep lookups for choosing between them.

Choosing which platforms to check

By default a lookup sweeps the whole band for its depth. Send platforms to check only the ones you name - useful when you care about one specific service and do not want to pay the latency of a full sweep. A narrowed lookup is dramatically faster: one platform answers in tens of milliseconds against roughly fourteen seconds for the full set.

POST /v2/lookup?wait=true
curl -X POST "https://api.digifootprint.dev/v2/lookup?wait=true" \
  -H "Authorization: Bearer dfp_your_key" \
  -H "Content-Type: application/json" \
  -d '{"query": "someone@example.com", "platforms": ["google", "github"]}'

The list overrides depth selection rather than narrowing within it, so you can name a platform that is not in the instant band and still get it. Combining it with ?instant=true keeps the latency ceiling and drops the band. It also implies ?wait=true: the queued path re-derives its own work and would discard your selection.

An unknown slug is rejected, not ignored. A typo returns 422 unknown_platforms naming exactly what was not recognised - silently checking fewer platforms than you asked for would tell you a service came back clean when it was never checked at all.

A platform that is real but cannot be reached from the input you sent - asking for reddit with an email address, when Reddit is keyed on a username - is skipped instead, and simply absent from results. One incompatible entry does not fail the whole request. Every slug and the inputs that reach it are listed by GET /v2/platforms.

The response

200 OK
{
  "lookupId": "4b6bbfec-e59d-45c5-961d-bc625b921db1",
  "input": { "email": "jane.doe@gmail.com", "phone": "+14155552671", "username": null, "ip": "45.83.91.1" },
  "risk": {
    "score": 23,
    "signals": [
      { "id": "ip.vpn", "module": "ip", "label": "Address belongs to a VPN service", "points": 15, "severity": "medium" },
      { "id": "cross.phone_ip_country", "module": "cross", "label": "Phone country and IP country differ", "points": 10, "severity": "medium" }
    ]
  },
  "footprint": {
    "platformsChecked": 2824,
    "matches": 47,
    "socialMatches": 46,
    "derivedMatches": 3,
    "hits": ["spotify", "duolingo", "pinterest", "..."],
    "servedFromCache": false,
    "results": [
      { "platform": "spotify", "registered": true, "method": "account_enumeration", "icon": "https://.../spotify.svg", "checkedAt": "2026-09-25T21:06:32.839Z", "identity": "email" },
      { "platform": "whatsapp", "registered": true, "method": "public_api", "icon": "https://.../whatsapp.svg", "checkedAt": "2026-09-25T21:06:31.102Z", "identity": "phone" }
    ],
    "breaches": [{ "name": "Duolingo", "date": "2023" }],
    "webMentions": null
  },
  "email": {
    "address": "jane.doe@gmail.com",
    "deliverable": true, "disposable": false, "freeProvider": true, "customDomain": false, "roleAccount": false, "acceptAll": false,
    "domain": { "name": "gmail.com", "ageDays": 11365, "validMx": true, "...": "..." },
    "breaches": { "listed": true, "count": 81, "firstSeenAt": "2012", "lastSeenAt": "2026", "records": [{ "name": "LinkedIn", "date": "2012-05-05" }, "..."] },
    "age": { "firstSeenAt": "2012-05-05", "ageDays": 5260, "source": "breach_records", "estimate": true },
    "risk": { "score": 5, "signals": [ "..." ] }
  },
  "phone": {
    "number": "+14155552671", "valid": true, "e164": "+14155552671", "countryCode": "US",
    "carrier": "T-Mobile USA", "lineType": "mobile", "isVoip": false, "...": "...",
    "risk": { "score": 8, "signals": [ "..." ] }
  },
  "ip": {
    "address": "45.83.91.1", "isVpn": true, "isHosting": true, "operator": "NordVPN", "...": "...",
    "location": { "countryCode": "SE", "city": "Stockholm", "...": "..." },
    "risk": { "score": 25, "signals": [ "..." ] }
  },
  "meta": {
    "tier": "standard",
    "modules": ["footprint:email", "footprint:phone", "email", "phone", "ip"],
    "instant": false,
    "deadlineMs": null,
    "platformsPending": null
  }
}

Absent is not negative. A platform we could not get a confident answer for is omitted from results entirely, rather than returned as registered: false. Not every one of the platforms we check will appear. A site that blocked us or timed out is an unknown, and we will not report an unknown as a fact.

footprint.hits is the short answer. hits lists the platforms that came back registered, so you do not have to filter results yourself. It always has exactly matches entries - both come from the same place - and it excludes derived-handle hits for the same reason matches does: those say a handle exists, not that this person owns it.

Modules are opt-in. The footprint - which platforms each identity is registered on, plus breaches and web mentions - runs by default. Email, phone and IP intelligence are switched on per request with checks; the keys are footprint, email, phone, ip and riskSignals. {"footprint": false} skips the sweep (and with it breaches and web mentions), which is how a module-only request answers in about a second.

Each module is a top-level object: email, phone and ip, each null when it did not run, each with its own risk. footprint is null when the sweep was switched off.

risk is one shape everywhere: { score, signals? }. The root risk.score is the total of every signal that fired, capped at 100; it is null when none of email, phone or IP ran. signals - the named rules that fired - come back only with checks.riskSignals: true on plans that include them. See risk scores.

null is not empty. footprint.breaches and footprint.webMentions are null when that source is not available on this deployment (it is not a plan feature - upgrading does not change it), which is a different fact from [] meaning we checked and found nothing. A breach date is the ISO date (YYYY-MM-DD) the breach occurred, not when it was disclosed.

footprint.lockedPlatforms is a paywall, not a miss. Present only when the account’s plan did not include some of the platforms this query could reach - today the free trial’s phone lookup, which checks WhatsApp only and lists Telegram, Facebook, Instagram and X here. Those platforms were not checked at all, are not counted in platformsChecked, and are unlocked by subscribing or topping up. Absent on paid accounts.

matches vs socialMatches. matches counts every registration. socialMatches excludes accounts that came with the email address rather than being signed up for. A Microsoft 365 tenant provisions an account for every employee, so that result is true of the whole company and says nothing about the individual. Those carry mailboxProvider: true. For fraud scoring, socialMatches is usually the number you want.

Listing past lookups

Scoped to your own account, most recent first. Limited to 20 requests a minute, see rate limits.

GET /v2/lookup
curl "https://api.digifootprint.dev/v2/lookup?limit=20&offset=0" \
  -H "Authorization: Bearer dfp_your_key"

GET /v2/lookup/:id returns the same shape as the synchronous response, with status (processing or completed).