back to console
$ /api/v1 · v1 stable

PRISM API

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.

Async · POST then pollBearer auth · prsm_live_…Per-key billing + rate limitsSigned webhooksBatch up to 50Idempotent retries
00

Endpoint map

quick reference
methodpathpurpose
GET/api/v1/healthservice heartbeat (no auth)
GET/api/v1/accountkey info · balance · rate budgets
GET/api/v1/usageper-key job + credit usage
POST/api/v1/tracersubmit one trace
POST/api/v1/tracer/autosmart-seed: pass any combo of selectors, server picks the best strategies
GET/api/v1/tracerlist jobs · cursor pagination · status filter
GET/api/v1/tracer/{id}poll a single job
DELETE/api/v1/tracer/{id}cancel + refund
POST/api/v1/tracer/batchsubmit up to 50 traces in one call
01

Authentication

bearer

Every 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.

02

Rate limits

per key

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).

tierendpointslimit
writePOST /tracer · DELETE /tracer/{id}60 / minute / key
batchPOST /tracer/batch10 / minute / key (each call may submit ≤50)
readGET /tracer · /tracer/{id} · /account · /usage600 / minute / key
healthGET /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.

03

Submit a trace

POST /api/v1/tracer

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.

depthwall time (approx)costresult quality
1~60s1× base
3~2m 10s3× base
5~3m 20s5× base
10~6m 25s10× base
25~14m 20s25× base
50~29m 25s50× base

Higher 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.

03b

Auto seed (smart cross-reference)

POST /api/v1/tracer/auto

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:

  1. Splits your seed into one search per primary selector type.
  2. Runs every search in parallel through the skip-trace engine (shallow first-page scrape per selector — no depth knob, the value is in cross-matching, not BFS depth).
  3. AI cross-references results across all searches. The target is the profile that matches the most of your original selectors — not necessarily the engine's rank #1 in any one search.
  4. Returns the canonical target on top, relatives below. Pass 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.

strategy_idfires whensearch query
by_phonephone providedphone alone
by_emailemail providedemail alone
by_name_addressname + addressname + full street
by_name_city_statename + city + state (+zip)name + locality
by_name_zipname + zip onlyname + zip
by_namename + nothing elsename fan-out
by_addressaddress without nameaddress alone
by_usernamehandle providedusername pivot
curl -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).

03c

Variable pricing

no results, no charge

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.

tierwhat you gotyou pay
emptyno usable target / engine couldn't identify a subject0% (full refund)
thinname + 1 weak signal40% of hold
standardname + multiple contacts, or contact + address75% of hold
richfull 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.

04

Idempotency

Idempotency-Key header

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 debit

Response carries Idempotent-Replayed: true on cache hits.

05

Seed shape

JSON object

The seed object accepts at least one of the following selectors. Combine multiple to disambiguate.

fieldexamplebehavior
emailj.doe@example.comdirect skip-trace
phone+1 415 555 0100direct skip-trace
nameJane Doepair with city/state
address1600 Amphitheatre Pkwy, Mountain View CA 94043single line
usernamejdoe87pivots through github / gravatar
domainexample.compivots through whois / dns / rdap
ip8.8.8.8pivots through ip-api / asn
dob1987-09-14narrows ambiguous matches
ssn_last44823narrows on identity overlap
city / state / zipOrem · UT · 84058address components
06

Cluster (AI) selection

cluster param

Control which AI cluster (or none) cross-references and organizes the dossier. The cluster choice multiplies the per-trace cost; see /account for your balance.

clustercost multnotes
{ kind: "managed", id: "off" }AI off · heuristic correlator only
{ kind: "managed", id: "managed:haiku" }Haiku 4.5 · structure only
{ kind: "managed", id: "managed:haiku+search" }Haiku 4.5 + web search
{ kind: "managed", id: "managed:sonnet+search" }Sonnet 4.6 + web search
{ kind: "byo", configId: <int> }Bring Your Own LLM (free uplift) — config in /settings
07

Cancel

DELETE /api/v1/tracer/{id}

Cancel 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 }
08

Batch submit

POST /api/v1/tracer/batch

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": "..." }
  ]
}
09

Poll a job

GET /api/v1/tracer/{id}

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_…"
10

List jobs

GET /api/v1/tracer

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
11

Account snapshot

GET /api/v1/account

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_…"
12

Usage stats

GET /api/v1/usage

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 }
  }
}
13

Webhooks

signed delivery

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)
14

Status lifecycle

state machine
  1. 1. queued — accepted, debit posted, awaiting worker
  2. 2. running — engine is fanning queries through the BFS frontier
  3. 3. doneresult populated; final state
  4. 3. failederror set; debit refunded automatically
  5. 3. cancelled — operator cancelled; debit refunded
15

Errors

HTTP status codes
codenamemeaning
202acceptedtrace queued; poll the returned id
400bad_requestmissing / malformed JSON, empty seed
401unauthorizedmissing / invalid / revoked bearer
402insufficient_creditstop up to continue · response includes balance + cost
404not_foundpolling a job that doesn't belong to this key
410already_terminalDELETE on a job that already finished/failed/cancelled
413batch_too_largePOST /tracer/batch with >50 items
429rate_limitedtier specified in body · check Retry-After
500internal_errorengine fault · job auto-fails + refunds
16

Doctrine

don't be a creep
PRISM only refracts already-publicrecords. Operators must have a stated lawful basis for every query (debt collection, fraud investigation, journalism, lawful process). Don't use this against private individuals without justification — see the doctrine for what we will and won't enable.