Bearer-auth REST API for the PRISM multi-stage skip tracer. Submit a seed, poll for the dossier, plug into any HTTP client. The same engine that powers the Lookup tab, exposed for headless / programmatic use.
GET/api/v1/healthservice heartbeat (no auth)GET/api/v1/accountkey info · balance · rate budgetsGET/api/v1/usageper-key job + credit usagePOST/api/v1/tracersubmit one tracePOST/api/v1/tracer/autosmart-seed: pass any combo of selectors, server picks the best strategiesGET/api/v1/tracerlist jobs · cursor pagination · status filterGET/api/v1/tracer/{id}poll a single jobDELETE/api/v1/tracer/{id}cancel + refundPOST/api/v1/tracer/batchsubmit up to 50 traces in one callEvery request needs a key in the Authorization header. Generate one in Settings → API keys. Plaintext is shown once on creation; only its sha256 is stored. Revoking a key invalidates it instantly.
Authorization: Bearer prsm_live_abc123… Content-Type: application/json
/api/v1/healthis the only endpoint that doesn't require auth.
Sliding-window in-memory limiter on every endpoint, keyed on your api key id. Exceed and you get 429 with Retry-After. Layered on top of the edge per-IP cap (120 req/min).
writePOST /tracer · DELETE /tracer/{id}60 / minute / keybatchPOST /tracer/batch10 / minute / key (each call may submit ≤50)readGET /tracer · /tracer/{id} · /account · /usage600 / minute / keyhealthGET /health240 / minute / IP (unauth)Every successful response returns X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (unix seconds). 429 responses also return Retry-After in seconds.
Returns immediately with a job id and status: queued. The engine runs asynchronously. Poll the status endpoint until the job terminates.
depth is a continuous integer between 1 and 50. Higher depth runs the BFS expansion deeper — more queries, more 2captcha solves, more wall time, but more identifiers per dossier.
1~60s1× base3~2m 10s3× base5~3m 20s5× base10~6m 25s10× base25~14m 20s25× base50~29m 25s50× baseHigher depth is slower but more accurate. Most subjects resolve clean at depth 3-5; depth 10+ is for cold cases and fraud-investigation grade dossiers.
curl -X POST https://prism-tools.vip/api/v1/tracer \
-H "Authorization: Bearer prsm_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-unique-id-123" \
-d '{
"seed": { "email": "jane@example.com" },
"depth": 5,
"useAi": true,
"cluster": { "kind": "managed", "id": "managed:sonnet+search" },
"webhookUrl": "https://your.app/prism-webhook"
}'Response (HTTP 202):
{
"job": { "id": "8f2c…", "status": "queued", "seed": {...}, "depth": 5, "use_ai": true, ... },
"charged": 20,
"balance": 80,
"poll_url": "/api/v1/tracer/8f2c…"
}Heads up: the charged value here is the submit-time hold (worst-case, assumes a rich dossier). On completion PRISM rescores the result and refunds the difference — see Variable pricing.
Don't know which selector will produce the cleanest dossier? Hand the orchestrator any combination of fields you have — phone, name, address, email, username — and it:
hideRelatives: true to drop relatives entirely.Why cross-match beats recency: rank #1 in any single search is the engine's recency pick (latest registered owner, current resident on HOA / utility records). But if you gave phone+address and address-search #2 happens to have a phone matching your seed phone, that #2 is your target — not address-search #1.
by_phonephone providedphone aloneby_emailemail providedemail aloneby_name_addressname + addressname + full streetby_name_city_statename + city + state (+zip)name + localityby_name_zipname + zip onlyname + zipby_namename + nothing elsename fan-outby_addressaddress without nameaddress aloneby_usernamehandle providedusername pivotcurl -X POST https://prism-tools.vip/api/v1/tracer/auto \
-H "Authorization: Bearer prsm_live_…" \
-H "Content-Type: application/json" \
-d '{
"seed": {
"phone": "+1 415 555 0100",
"name": "Jane Doe",
"address": "1600 Pennsylvania Ave NW, Washington DC"
},
"useAi": true,
"cluster": { "kind": "managed", "id": "managed:haiku+search" },
"hideRelatives": false,
"maxStrategies": 5
}'Response on success (HTTP 200):
{
"target": { /* full ProfileRecord — the canonical pick */ },
"relatives": [ /* relatives from the picked profile, [] when hideRelatives */ ],
"cross_reference": {
"strategy_id": "by_address",
"rank_in_search": 2,
"cross_matched_selectors": ["phone","name","address"],
"reasoning": "by_address rank #2 has phone matching seed phone and name matching seed name — pick over by_address #1 (recency-only match)",
"ai": { "provider": "managed", "model": "claude-haiku-4-5-…", "usage": {...} }
},
"attempts": [
{ "strategy_id": "by_phone", "selector_used": "phone", "query_text": "+1 415 555 0100", "tier": "standard", "target_name": "Bob Reyes", "alternate_count": 3 },
{ "strategy_id": "by_name_address", "selector_used": "name + address", "query_text": "Jane Doe | 1600 Penn …", "tier": "rich", "target_name": "Jane Doe", "alternate_count": 1 },
{ "strategy_id": "by_address", "selector_used": "address", "query_text": "1600 Pennsylvania Ave …", "tier": "rich", "target_name": "Jane Doe", "alternate_count": 2 }
],
"total_charged": 6,
"balance": 94
}If every search returned empty you get HTTP 422 with error: "no_results" and total_charged: 0. AI is never charged on dead traces — the cross-ref step is skipped entirely, and the per-search refunds zero out the search costs.
useAi: false makes the cross-reference deterministic (free) — a heuristic that scores each candidate by how many of your original selectors it matches.hideRelatives: true drops the relatives array from the response.maxStrategies caps how many parallel searches run (1–6, default 5).
PRISM holds the worst-case cost at submit time but only charges based on the quality of the dossier the engine actually produces. The trace cost in your wallet is reconciled the moment the engine returns.
emptyno usable target / engine couldn't identify a subject0% (full refund)thinname + 1 weak signal40% of holdstandardname + multiple contacts, or contact + address75% of holdrichfull dossier: contacts + addresses + relatives + records100% (no refund)This applies system-wide — single tracer, batch, auto, web UI. If the engine errors out (network fault, captcha unsolvable), the operator is fully refunded as well. The cost field on a finalized lookup row reflects the final charge after refund.
Send Idempotency-Key: <opaque-string> on any POST. PRISM caches the response for 24 hours scoped to (api_key, idempotency_key). Replays return the cached body without re-executing — safe for client retries on timeout / network blip.
# initial
curl -X POST https://prism-tools.vip/api/v1/tracer \
-H "Authorization: Bearer prsm_live_…" \
-H "Idempotency-Key: trace-2026-05-06-abc" \
... → 202 { job: {...}, charged: 1 }
# retry within 24h with the same key
curl -X POST https://prism-tools.vip/api/v1/tracer \
-H "Authorization: Bearer prsm_live_…" \
-H "Idempotency-Key: trace-2026-05-06-abc" \
... → 202 (cached) { job: {same id}, ... } # no second debitResponse carries Idempotent-Replayed: true on cache hits.
The seed object accepts at least one of the following selectors. Combine multiple to disambiguate.
emailj.doe@example.comdirect skip-tracephone+1 415 555 0100direct skip-tracenameJane Doepair with city/stateaddress1600 Amphitheatre Pkwy, Mountain View CA 94043single lineusernamejdoe87pivots through github / gravatardomainexample.compivots through whois / dns / rdapip8.8.8.8pivots through ip-api / asndob1987-09-14narrows ambiguous matchesssn_last44823narrows on identity overlapcity / state / zipOrem · UT · 84058address componentsControl which AI cluster (or none) cross-references and organizes the dossier. The cluster choice multiplies the per-trace cost; see /account for your balance.
{ kind: "managed", id: "off" }0×AI off · heuristic correlator only{ kind: "managed", id: "managed:haiku" }1×Haiku 4.5 · structure only{ kind: "managed", id: "managed:haiku+search" }2×Haiku 4.5 + web search{ kind: "managed", id: "managed:sonnet+search" }4×Sonnet 4.6 + web search{ kind: "byo", configId: <int> }1×Bring Your Own LLM (free uplift) — config in /settingsCancel a job that hasn't terminated. Refunds the credit debit (idempotent). Returns 410 if the job is already done, failed, orcancelled.
curl -X DELETE https://prism-tools.vip/api/v1/tracer/8f2c… \
-H "Authorization: Bearer prsm_live_…"
# 200 { job: {...status: "cancelled"}, refunded: 4 }Up to 50 traces per call. Each item is independently debited and queued. Results pair back to inputs by index. Honors Idempotency-Key for the whole batch.
curl -X POST https://prism-tools.vip/api/v1/tracer/batch \
-H "Authorization: Bearer prsm_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: nightly-import-2026-05-06" \
-d '{
"items": [
{ "seed": { "email": "a@x.com" }, "depth": 1 },
{ "seed": { "phone": "+14155550100" } },
{ "seed": { "name": "Jane Doe", "address": "Boston, MA" } }
]
}'
# 202
{
"submitted": 3, "failed": 0,
"results": [
{ "ok": true, "index": 0, "job": {...}, "charged": 2, "balance": 98, "poll_url": "..." },
{ "ok": true, "index": 1, "job": {...}, "charged": 2, "balance": 96, "poll_url": "..." },
{ "ok": true, "index": 2, "job": {...}, "charged": 2, "balance": 94, "poll_url": "..." }
]
}Returns the job in its current state. result is null until done.
curl https://prism-tools.vip/api/v1/tracer/8f2c… \ -H "Authorization: Bearer prsm_live_…"
Cursor pagination via before=<ISO>. Status filter via status=.
curl "https://prism-tools.vip/api/v1/tracer?status=done&limit=50" \ -H "Authorization: Bearer prsm_live_…" # response includes meta.next_cursor (ISO timestamp); pass it as ?before=… for the next page
Useful at app boot — lists key info, balance, rate-limit budget, and the webhook secret derivation hint.
curl https://prism-tools.vip/api/v1/account \ -H "Authorization: Bearer prsm_live_…"
Counts by status across rolling 24h / 7d windows, credits spent, average duration of completed jobs. For dashboards.
curl https://prism-tools.vip/api/v1/usage \
-H "Authorization: Bearer prsm_live_…"
{
"usage": {
"jobs_24h": { "queued": 3, "running": 1, "done": 41, "failed": 2 },
"jobs_7d": { "done": 287, "failed": 14, ... },
"credits_spent": { "day": 47, "week": 312 },
"duration_done_7d": { "avg_ms": 312000, "max_ms": 891000, "n": 287 }
}
}Pass webhookUrl on submit (single or batch) and PRISM will POST the terminal job (done / failed) to that URL with retries (3 attempts, exponential backoff, 30s per-attempt timeout). HTTPS only; SSRF-blocked against private/loopback/cloud-metadata hosts.
Headers PRISM sends:
POST https://your.app/prism-webhook
content-type: application/json
user-agent: PRISM-Webhook/1.0
x-prism-event: trace.done | trace.failed
x-prism-event-id: <job uuid> (idempotency)
x-prism-timestamp: 1714972800
x-prism-signature: t=1714972800,v1=<hex_hmac>
{
"event": "trace.done",
"job_id": "8f2c…",
"status": "done",
"seed": {...},
"duration_ms": 287214,
"records_count": 7,
"result": {...},
"error": null,
"finished_at": "..."
}Verifying the signature: the secret is sha256(your_api_key). PRISM signs <timestamp>.<rawBody> with HMAC-SHA-256. Reject events older than ~5 minutes.
JavaScript verifier:
import crypto from "node:crypto";
function verifyPrismWebhook(rawBody, header, apiKey, toleranceSec = 300) {
if (!header) return false;
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!Number.isFinite(t)) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - t) > toleranceSec) return false;
const secret = crypto.createHash("sha256").update(apiKey).digest("hex");
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
try {
return crypto.timingSafeEqual(Buffer.from(parts.v1, "hex"), Buffer.from(expected, "hex"));
} catch { return false; }
}Python verifier:
import hashlib, hmac, time
def verify_prism_webhook(raw_body: bytes, header: str, api_key: str, tolerance_sec: int = 300) -> bool:
if not header: return False
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", 0))
if abs(time.time() - t) > tolerance_sec: return False
secret = hashlib.sha256(api_key.encode()).hexdigest()
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts.get("v1", ""), expected)queued — accepted, debit posted, awaiting workerrunning — engine is fanning queries through the BFS frontierdone — result populated; final statefailed — error set; debit refunded automaticallycancelled — operator cancelled; debit refunded202acceptedtrace queued; poll the returned id400bad_requestmissing / malformed JSON, empty seed401unauthorizedmissing / invalid / revoked bearer402insufficient_creditstop up to continue · response includes balance + cost404not_foundpolling a job that doesn't belong to this key410already_terminalDELETE on a job that already finished/failed/cancelled413batch_too_largePOST /tracer/batch with >50 items429rate_limitedtier specified in body · check Retry-After500internal_errorengine fault · job auto-fails + refunds