OSINTverseDocs

Search workflow

Create a SearchIn job, then poll until it completes. FaceCheck and OSINT Industries are async.

Markdown version

Every search returns a job object with id, status, cost_usd, and balance_after.

Status values

StatusMeaning
completedFinished. Results are in result.matches and result.sources.
runningStill processing. Poll GET /v1/search/:id every ~2 seconds.
failedInsufficient balance or request rejected before charging.
refundedUpstream provider failed after billing. Balance was restored.

Synchronous providers

LeakRadar, Predicta Search, DeHashed, Snusbase, Whoxy, WhoisXML, SecurityTrails, Shodan, BuiltWith, Picarta, and LeakOSINTbot typically complete in the POST /v1/search response. Check status anyway — treat running as a signal to poll.

Hunter.io Domain Search and Email Finder are usually synchronous. Email Verifier can return running while Hunter finishes SMTP checks — poll GET /v1/search/:id the same way as FaceCheck.

Hunter.io name queries must include a company domain: Alexis Ohanian reddit.com (or Jane Doe @ example.com).

Async polling (OSINT Industries)

OSINT Industries lookups return running immediately with poll_url. Keep calling GET /v1/search/:id until completed, failed, or refunded. result.matches grows as each module finishes. A clean stream end that simply omitted slow modules is still completed.

Stop an in-flight OSINT Industries job with POST /v1/search/:id/cancel. Modules already returned stay on the job; the original charge is kept. Jobs that are already completed, failed, or refunded are a no-op.

Running OSINT Industries response
{
  "id": "550e8400-e29b-41d4-a716-446655440002",
  "status": "running",
  "provider": "osint-industries",
  "input_type": "email",
  "query": "user@example.com",
  "cost_usd": "0.60",
  "balance_after": "9.40",
  "result": {
    "sources": [{ "name": "osint-industries" }],
    "matches": []
  },
  "error": null,
  "poll_url": "/v1/search/550e8400-e29b-41d4-a716-446655440002"
}

In the Python SDK, pass wait=True on client.search.create(...) or call client.search.wait(id). Without wait, poll yourself and read growing result.matches.

Async polling (FaceCheck)

Facecheck.id image searches often return running with poll_url. Keep calling GET /v1/search/:id until completed, failed, or refunded. FaceCheck result is typically null until the job completes. In the Python SDK, pass wait=True on client.search.create(...) or call client.search.wait(id).

Running response
{
  "id": "550e8400-e29b-41d4-a716-446655440001",
  "status": "running",
  "provider": "facecheck",
  "input_type": "image",
  "query": "https://example.com/photo.jpg",
  "cost_usd": "0.50",
  "balance_after": "9.45",
  "result": null,
  "error": null,
  "poll_url": "/v1/search/550e8400-e29b-41d4-a716-446655440001"
}

Completed FaceCheck matches use score, url, and thumbnail (often a data URI).

Do not busy-loop

Poll about every two seconds. Daily key rate limits still apply to poll requests.

Result shape

result.sources describes what the adapter fetched (counts, paging, lookup type). result.matches is the normalized list your UI or pipeline should consume. Provider-specific fields stay on each match — see provider guides.

LeakRadar unlocks

A LeakRadar search charges $1.00 and auto-unlocks the first 1,000 credentials. Remaining matches unlock from the same job with POST /v1/search/:id/unlock at $0.50 per 1,000 (billed in 1,000-result blocks). The SearchIn UI shows leftover count and cost before you confirm.

OSINT Industries premium modules

An OSINT Industries search charges $0.60 for the basic module scan. Premium modules are excluded by default. Optional premium modules (Snapchat, TikTok, and similar) add $1.80.

  • At create time, send "premium": true on POST /v1/search (or on bulk, where it applies only to OSINT Industries jobs). Combined cost is $2.40.
  • After a completed basic search, call POST /v1/search/:id/premium with { "premium": true }. The extra $1.80 is charged then; new modules merge into the stored result.
  • Already-premium searches return 200 with no extra charge. Insufficient balance returns 402. Upstream failure refunds the extra $1.80 only.

The SearchIn UI offers a checkbox before you run, and a result-panel button after a basic search completes.

POST /v1/search/bulk runs a cartesian product of queries[] × providers[] in one request.

LimitValue
Max queries50
Max jobs (queries × providers)100
Bulk request
{
  "input_type": "email",
  "queries": ["alice@example.com", "bob@example.com"],
  "providers": ["leakradar", "dehashed"]
}

The response includes batch_id, estimated_cost_usd, summary, and a searches[] array (one job per pair, queries outer / providers inner). Partial success is normal — inspect each job’s status / error.

Poll any running job with GET /v1/search/:id the same way as a single search. The bulk call counts once toward the daily API key rate limit; each poll after it counts once too.

Webhooks

Pass callback_url on POST /v1/search or POST /v1/search/bulk (one event per job) to get a signed POST when the search finishes, instead of polling. The event says what happened, not what was found: fetch the result with GET /v1/search/:id and your key.

Request
{
  "provider": "facecheck",
  "input_type": "image",
  "query": "https://example.com/photo.jpg",
  "callback_url": "https://hooks.example.com/osintverse"
}
Event body
{
  "type": "search.completed",
  "timestamp": 1792040551,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "batch_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "completed",
    "provider": "facecheck",
    "input_type": "image",
    "cost_usd": "1.00",
    "refunded": false,
    "url": "/v1/search/550e8400-e29b-41d4-a716-446655440000"
  }
}

type is search.completed, search.failed, or search.refunded. The event never contains the query or the results.

Rules for callback_url: https on port 443, no credentials in the URL, and a public host. Internal names and private, loopback, or link-local addresses are refused when you create the search, and again on every delivery attempt. Redirects are not followed.

Delivery: three attempts (at once, after 5 seconds, after 30 seconds). Answer with any 2xx. A 4xx other than 408, 409, 425, or 429 stops the retries. Delivery is best effort, so keep GET /v1/search/:id as the source of truth.

Verify the signature

Events follow Standard Webhooks. Get your secret once with GET /v1/webhooks/secret (any key; it never charges). It is one secret per wallet: a team key gets the team's secret.

Each request carries webhook-id, webhook-timestamp, and webhook-signature. The signature is v1, followed by the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64-decoded part of the secret after whsec_. Reject timestamps more than five minutes old, and use webhook-id (stable per search and outcome) to drop duplicates.

Python
import base64, hashlib, hmac, time

def verify(secret: str, headers: dict, body: bytes) -> bool:
    msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
    if abs(time.time() - int(ts)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    expected = base64.b64encode(
        hmac.new(key, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest()
    ).decode()
    sent = [part.split(",", 1)[1] for part in headers["webhook-signature"].split() if part.startswith("v1,")]
    return any(hmac.compare_digest(expected, value) for value in sent)
Node
import { createHmac, timingSafeEqual } from "node:crypto"

export function verify(secret: string, headers: Record<string, string>, body: string) {
  const id = headers["webhook-id"]
  const ts = headers["webhook-timestamp"]
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64")
  const expected = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest()
  return headers["webhook-signature"]
    .split(" ")
    .filter((part) => part.startsWith("v1,"))
    .some((part) => {
      const sent = Buffer.from(part.slice(3), "base64")
      return sent.length === expected.length && timingSafeEqual(sent, expected)
    })
}

The official Standard Webhooks libraries (standardwebhooks on PyPI and npm) verify these events too.

On this page