SIRVEILEST. 2025 · SCAN API Get a key

API reference

Two endpoints. One rate card. Ship in an afternoon.

$0.10 a completed check. $0.35 a completed sweep of all 548 broker domains. $0.00 for anything that fails — errors and timeouts are our cost, not yours. Everything below is readable without an account.

Preview build — figures locked at launch.

Quickstart

Base URL https://api.sirveil.ai. Auth is one bearer key. No SDK to install, no OAuth dance, no sandbox to apply for — your first real answer is one curl away. Grab a key at /scan-api/signup and the meter starts at zero.

Quickstart
# A check — one person, one site you name. Built for broker
# and people-search domains; "couldn't tell" is a real answer.
# Identity fields go inside "identity"; domain stays top-level.
curl https://api.sirveil.ai/v1/verify \
  -H "Authorization: Bearer sv_live_…" \
  -d '{ "identity": { "firstName":"Jane", "lastName":"Doe",
         "phone":"5035551212" },
       "domain":"examplebroker.com" }'

# → one structured answer, with its evidence
{
  "status": "indexed",
  "domain": "examplebroker.com",
  "evidence": [ … ],
  "dropped_fields": [],
  "billed": "$0.10"   // failed calls: $0.00
}

That call billed ten cents. If it had failed, it would have billed nothing. That is the entire commercial relationship — the rest of this page is detail.

Authentication

Every request carries one header. Keys look like sv_live_… and come from /scan-api/signup — minutes, not meetings. The key is the account: it identifies you, it meters you, and that is all it does.

Header
Authorization: Bearer sv_live_…
  • Keep it server-side. A bearer key in browser JavaScript is a bearer key you have published. Proxy calls through your backend.
  • Rotate freely. Issue a new key, move traffic, revoke the old one — the meter follows the account, not the key.
  • Missing or bad key → 401, billed $0.00, like every error.

Building a good request

Both endpoints take the same identity shape: a JSON identity object holding everything we know about the subject. Only identity.firstName and identity.lastName are required — but the name only gets you through the door. What makes the answer worth paying for is one strong identifier alongside it. A request carrying a name and a phone number is a materially different product experience from a request carrying a name alone.

Which identifier to send first

We rank identifiers by how well they actually retrieve, measured against the pages we check — not by how selective they look on paper. The ranking below is the order the search actually values them, on both endpoints:

RankIdentifierWhy it ranks there
1stphoneBroker profile pages are keyed on phone numbers. The single best field to send.
2ndemailNear-unique across the open web. A strong second if you have no phone.
3rdstreetAddress1 + city/stateStrong — broker pages are keyed on address too — but it’s the field people are most reluctant to hand over on a first pass.
name onlyThe fallback. It works, but this is the weak case — see the floor warnings on each endpoint.

Why phone outranks email here. On a general web search, an email address is the more selective identifier — a bare email query is near-unique across the whole internet. But once a query is scoped to a single broker domain, that ranking inverts: broker profile pages are built around phone numbers and addresses, and very often don’t print an email at all. So the search leads with phone deliberately, against the more obvious ordering. The ranking was measured, not assumed.

  • city + state are the cheapest defence against namesake collapse. For any common name, they’re the difference between an answer about one person and an answer about four.
  • streetAddress1 is recommended, not minimal — send it when you have it, but don’t let it stand between you and your first call.
  • usernames extends the search across a large list of public sites. It only runs on a handle you declare — we never guess one for you.

Fields we can’t use: dropped_fields

A field we can’t use — a malformed phone number, an unparseable date of birth — is dropped and named in the dropped_fields array on the response. It never fails the whole request, and it is never silently ignored: a silently-ignored field would mean you paid full price for a weaker search with no way to find out. Naming the dropped field is how you find out.

Integration tip: log dropped_fields during your build-out. It’s the fastest way to catch a formatting mismatch between your data model and ours — and it turns a vague “the results seem thin” ticket into a one-line fix on your side.

POST/v1/verify

The check — one person, one domain you name. Synchronous. Measured 890 ms median, 1,463 ms p95 (benchmarks).

Ask whether a person is indexed on a single site. Broker and people-search domains return decidable verdicts; login-walled or noindex’d sites return an honest indeterminate instead of a fake “no”. Every answer arrives with its evidence.

Request body

Identity fields travel inside one identity object; domain stays top-level. This is the body we recommend sending — four fields, phone-led:

Recommended — 4 fields
{
  "identity": {
    "firstName": "Jane",
    "lastName": "Doe",
    "phone": "5035551212"
  },
  "domain": "examplebroker.com"
}

Phone is the top-ranked identifier on this endpoint — broker profile pages are keyed on phone numbers — so this shape produces an identifier-anchored query instead of the name-only fallback (see Building a good request). No phone? Substitute email. Neither? streetAddress1 plus city.

Minimum — 3 fields (the floor, not the recommendation)
{ "identity": { "firstName": "Jane", "lastName": "Doe" }, "domain": "examplebroker.com" }

The floor works, but don’t lead with it. A name-only request runs the weakest possible search at full price, and it’s the request most likely to answer indeterminate — a technically successful call that feels like a failure. Send one strong identifier with the name.

FieldTypeRequiredNotes
identity.firstNamestringrequiredSubject’s given name, as a broker would list it.
identity.lastNamestringrequiredSubject’s family name.
identity.phonestringoptionalTop-ranked identifier here. Supplying phone or email is what turns a name-only fallback into an identifier-anchored search.
identity.emailstringoptionalSecond-ranked. Near-unique on the open web; less so on broker pages, which rarely print one.
identity.citystringoptionalWith state, the cheapest defence against namesake collapse on common names.
identity.statestringoptionalTwo-letter US state, e.g. "OR".
identity.streetAddress1stringoptionalRecommended, not minimal — the third identifier shape, strong when supplied.
identity.dateOfBirthstringoptionalHelps confirm a match. Unparseable values are dropped and named in dropped_fields.
identity.employerstringoptionalExtra disambiguation when brokers list one.
identity.usernamesarrayoptionalExtends the search across public sites — only for handles you declare; we never guess one.
domainstringrequiredTop-level, next to identity. The site to check, bare hostname — e.g. examplebroker.com. Any public site is accepted; decidability is class-dependent (see Statuses).

Why the name is required. There are no phone-only or email-only lookups — ever, on any tier. The name is the anchor every candidate record is scored against; without it, nothing decides whether the record we found is the right human being. Other vendors will return a match on a phone number without ever telling you how confident they are that it’s the right person. We won’t. One strong identifier alongside the name is what makes the result worth paying for.

Response

200 OK
{
  "status": "indexed",            // indexed | not_indexed | indeterminate
  "domain": "examplebroker.com",
  "evidence": [
    {
      "source": "https://examplebroker.com/profile/jane-doe-tx-1982",
      "query": "\"Jane Doe\" site:examplebroker.com",
      "observed_at": "2026-08-16T14:02:11Z"
    }
  ],
  "dropped_fields": [],           // any unusable inputs, named — never silently ignored
  "billed": "$0.10"              // failed calls: $0.00
}

dropped_fields lists any identity fields we couldn’t use — a malformed phone, an unparseable date of birth — dropped and named rather than failing the request or vanishing silently. See Building a good request.

Billing: $0.10 per completed check — and indeterminate is a completed check, because “public search can’t see it” is a real answer. Errors and timeouts bill $0.00, always.

POST/v1/scan

The full sweep — one person, all 548 registry domains. Asynchronous job. Measured 154 s median.

A sweep runs your subject across the full 548-domain registry — derived from the California CPPA Data Broker Registry, the legally mandated filing list, plus curation (derivation published). It is real work, so you submit it as a job and collect the report when it’s done. At $0.35 flat, that’s about $0.00064 per domain checked.

Request body

Same shape as a check: identity fields inside one identity object. This is the body we recommend for a sweep — six fields:

Recommended — 6 fields
{
  "identity": {
    "firstName": "Jane",
    "lastName": "Doe",
    "phone": "5035551212",
    "email": "[email protected]",
    "city": "Portland",
    "state": "OR"
  }
}

Every field earns its place: the name clears the gate; phone and email are what the identifier-led query search actually leans on; city and state are the cheapest defence against namesake collapse — on a common name, the difference between a report about one person and a report about four. Details in Building a good request.

Minimum — 2 fields (the floor, not the recommendation)
{ "identity": { "firstName": "Jane", "lastName": "Doe" } }

The floor works, but don’t lead with it. A name-only sweep costs the same $0.35 as any other and runs the weakest possible search — full price for the configuration least likely to find what’s out there, and the one most likely to answer indeterminate where a stronger request would have decided. Send one strong identifier with the name.

FieldTypeRequiredNotes
identity.firstNamestringrequiredSubject’s given name.
identity.lastNamestringrequiredSubject’s family name.
identity.phonestringoptionalTop-ranked identifier. Supplying phone or email is what turns a name-only sweep into an identifier-anchored one.
identity.emailstringoptionalSecond-ranked identifier; near-unique across the open web.
identity.citystringoptionalWith state, defends against namesake collapse on common names.
identity.statestringoptionalTwo-letter US state, e.g. "OR".
identity.streetAddress1stringoptionalRecommended, not minimal — the third identifier shape; broker profile pages are keyed on address.
identity.dateOfBirthstringoptionalHelps confirm a match. Unparseable values are dropped and named in dropped_fields.
identity.employerstringoptionalExtra disambiguation when brokers list one.
identity.usernamesarrayoptionalExtends the sweep across a large list of public sites — only for handles you declare; we never guess one.
webhook_urlstringoptionalTop-level, next to identity. HTTPS URL to POST the finished report to — see Webhooks (draft surface).

Response

202 Accepted
{ "job_id": "job_9f2c…", "status": "queued" }

Poll GET /v1/jobs/:job_id every few seconds, or pass webhook_url and let us call you. Median finish is about two and a half minutes; slowest measured run was 203 seconds.

Billing: $0.35 per completed sweep, billed when the job finishes — not when it’s queued. A job that errors out or times out bills $0.00.

GET/v1/jobs/:job_id

Collect a sweep — the finished report, or the job’s progress if it’s still running.

While the sweep runs, status is queued or running. When it flips to complete, the full report is in the body: one entry per domain, every entry with a verdict and its evidence. Fetching a job is free — poll as much as you like.

Response

200 OK · complete
{
  "job_id": "job_9f2c…",
  "status": "complete",          // queued | running | complete | failed
  "summary": {
    "indexed": 5,
    "not_indexed": 522,
    "indeterminate": 21          // always sums to 548
  },
  "results": [
    { "domain": "examplebroker.com", "status": "indexed", "evidence": [ … ] },
    { "domain": "quietbroker.example", "status": "not_indexed", "evidence": [ … ] }
    // … 546 more — every domain in the registry reports, with its source
  ],
  "billed": "$0.35"              // a failed job: $0.00
}

Billing: the $0.35 belongs to the sweep, not the fetch. GET /v1/jobs/:id itself is free at any polling frequency.

Statuses & evidence

Every verdict is one of three strings. There is no fourth, and there is no “probably”.

indexed

Public search can see a page for this subject on this domain. The evidence array says exactly where and how.

not_indexed

Public search cannot find this subject on this domain — a decidable, checkable negative, with the queries that came back empty.

indeterminate

The domain doesn’t expose subject pages to public search — login-walled or noindex’d — so no honest yes/no exists. We say so instead of guessing.

Decidability is class-dependent, and we tell you which class you’re in. Data-broker and people-search domains return decidable verdicts, because being publicly findable is their business model. Login-walled or noindex’d sites return indeterminate — if public search can’t see the subject page, neither can we, and neither can the stranger you’re worried about. An indeterminate is a completed, billed answer; a failed call is an error or timeout and bills $0.00.

Negatives are the product. A typical subject’s sweep reads roughly 5 indexed / 522 not indexed / 21 indeterminate. Those 522 checkable no’s — each with the query that came back empty — are what lets you tell a customer “you’re clear here” and prove it.

The evidence array

Each finding shows its work: where it came from, what we asked, and when we looked. Sources are public URLs — public queries only, no logins, no back doors. We check what public search can see, because that’s what an abuser or a stranger can see.

Evidence entry
{
  "source": "https://examplebroker.com/profile/jane-doe-tx-1982",  // public URL we saw
  "query": "\"Jane Doe\" site:examplebroker.com",                 // the query we ran
  "observed_at": "2026-08-16T14:02:11Z"                            // when we looked (UTC)
}

PreviewThe three fields above — source URL, query used, observed-at timestamp — are the committed shape; exact field names are locked at launch.

Errors

Errors are JSON, they say what went wrong in words, and they never bill. One shape, everywhere:

Error body
{
  "error": {
    "type": "identity_incomplete",
    "message": "identity.firstName and identity.lastName are required — the name is the anchor every match is scored against.",
    "doc_url": "https://sirveil.ai/scan-api/docs#errors"
  }
}
StatusMeaningBilled
400Malformed request — broken JSON, missing required field. Named types below.$0.00 — never
401Missing, revoked, or wrong API key.$0.00 — never
404No such route, or no such job_id on your account.$0.00 — never
422Valid JSON, unusable values — e.g. a domain that isn’t a hostname.$0.00 — never
429Too many requests. Back off and honor Retry-After.$0.00 — never
500Our fault. Safe to retry; if it persists, check /scan-api/status.$0.00 — never

Named error types

TypeStatusWhat it means
identity_incomplete400The request is missing a name. The API’s actual message: “identity.firstName and identity.lastName are required — the name is the anchor every match is scored against.” Both endpoints return it.
invalid_domain400POST /v1/verify only: the top-level domain is missing or isn’t a usable bare hostname. Any public hostname is accepted — there is no allowlist — but it has to be a hostname.

Both are refused before any search runs — no work is done, nothing is billed, and the recorded cost is null, not zero: a malformed request cost us nothing to answer and we will not assert a measurement we never made.

The billing column is not a courtesy footnote — it is the pricing model. You are billed per completed answer, and an error is not an answer. Infrastructure trouble is our cost, not yours.

Rate limits

Generous by default, raised on request. Normal integration traffic — including polling GET /v1/jobs/:id while a sweep runs — will not meet the limiter. If you hit 429, the response carries a Retry-After header; honor it and you’re fine.

PreviewMeasured default limits will be published here before launch — we publish numbers we’ve tested, not numbers we hope for. Running hot on purpose? Volume pricing and raised limits: [email protected].

Billing semantics

A meter and a monthly invoice. That is the whole apparatus.

  • Pure postpaid. Every completed answer increments the meter: $0.10 a check, $0.35 a sweep. At month’s end you’re invoiced for what the meter read. No packs, no credits, no subscriptions, no minimums.
  • Zero calls, zero dollars. A quiet month produces no charge and no invoice. Nothing is prepaid, so nothing can expire and no money of yours sits on our books.
  • Marketplace metering or direct card. Buy through AWS Marketplace (preferred) or Azure Marketplace (MACC-eligible) and usage lands on your existing cloud bill through the marketplace meter. Or put a card on file and we invoice directly. Same public rates at every door.
  • The published rates are the ceiling. Committed volume gets private offers below them — [email protected].
  • Errors bill $0.00 — see Errors. The full worked math lives at /for-business#math.

Versioning & deprecation

/v1 is the stable surface: the endpoints, request fields, statuses, and error envelope on this page. We add fields without warning — parse tolerantly and ignore what you don’t recognize. We do not remove or repurpose anything under /v1 without a breaking-change process:

  • At least 6 months’ notice before any breaking change takes effect.
  • Announced on /scan-api/changelog first, before any other channel.
  • A breaking change ships as /v2; /v1 keeps answering through the notice window.

Webhooks draft

PreviewThis surface is drafted, not final. Everything below may change before launch; it will be confirmed on /scan-api/changelog.

Pass webhook_url when you submit a sweep and we POST the finished report to you instead of making you poll. Deliveries are signed with HMAC-SHA256 over the raw body, keyed to your API key, in the Sirveil-Signature header — verify it before trusting the payload.

Delivery (draft)
POST https://yourapp.example/hooks/sirveil
Sirveil-Signature: sha256=6b4f…       // HMAC-SHA256 of the raw body
Content-Type: application/json

{ "job_id": "job_9f2c…", "status": "complete", "summary": { … }, "results": [ … ] }

Polling GET /v1/jobs/:id works today and keeps working — webhooks are a convenience, not a dependency.

Where next

See the receipts

890 ms median checks and 154 s sweeps are measured claims, run by run, slow runs included.

Benchmarks →

Run the math

The interactive meter: sliders, an example invoice, and the whole rate card on one page.

Pricing →

Get a key

Minutes, not meetings. The meter starts at zero and stays there until you call.

Sign up →