HuntPeople Docs
Docs/Overview

Getting started

The short version of how a search actually works.

Give HuntPeople a name, email, phone, or handle, and it chains lookup tools together until it identifies the person, then keeps going to fill in what you actually asked for.

Identification (finding the person) and enrichment (filling in the rest) are two phases with different stop conditions. Identification stops the moment confidence crosses its threshold, since that's exactly what it's producing. Enrichment can't use that signal, identity is already settled, so it stops on budget or on diminishing returns instead, or once every goal is met. That's a fourth, separate stop reason (goals-met) from the three that apply to identification (confidence threshold, budget cap, diminishing returns).

What you already know becomes seeds. What you actually want decides when the search is done, anything else it happens to find is kept but never chased for its own sake. Say nothing and the default goal set runs instead, see the FAQ.

When free lookups aren't enough, HuntPeople can go further with Deep Search. See the Billing page for what that includes on your plan.

Database & Work Mode

Every investigation writes into one shared person record per real human, not one record per investigation, so a second search on someone already found picks up where the first left off instead of starting over.

The Database page has two views of the same data: Investigation Mode for the full record (facts, excluded candidates, a written summary), Work Mode for actually reaching out (contact status, notes, one-tap call/email). Edits, deletes, and restores made here are scoped to your team: they never touch the canonical record another team sees, and a bad merge can always be split back apart, since every fact remembers which investigation produced it.

API reference

The API is live. Everything below is checked against the running service, and the frontend generates its types from the same OpenAPI schema.

Authentication

Send your key as a bearer token on every request. Base URL: https://api.huntpeople.ai (running the stack locally, it is http://localhost:8000).

Authorization: Bearer sk_live_...

A key is scoped to the team it was created in, and acts as the account that created it. Most of the API takes a key. The endpoints marked Signed in are dashboard-only, because they manage the account itself (teams, invites, notifications, buying credits, creating more keys); a key gets 401 on those. Staff marks HuntPeople's own operator views.

Endpoints

POST /v1/search/previewParse a natural-language query or a CSV's column mapping without starting anything, the same confirm-before-run step the UI shows before committing.
POST /v1/searchStart a search for one person. The want list is the goal: the search stops once those fields are found (anything else it passes is still kept). Omit it and the default goal set runs (phone, email, company, domain, and a social profile link, never a name). Set exhaustive: true for an exhaustive check that ignores the goal and digs until the token budget runs out. Three independent axes: mode is quick, normal or deep and picks the model; token_budget is the token stop and is snapped up to 100000, 200000 or 300000; want/exhaustive decides when it is done. Defaults are normal and 100000. Price follows all three: a goal-driven search is priced by model alone, an exhaustive one by model and budget together.
POST /v1/searches/estimateWhat a list will cost in credits before you run it: how many rows are searchable, how many credits that needs, and how many you have. Spends nothing and starts nothing.
POST /v1/searches/batchStart a search for a list of people. There is no row limit; your credit balance is the limit, so price it with /v1/searches/estimate first. The same want/exhaustive goal and mode apply to every person in the batch.
GET /v1/investigations/{id}Poll status, stop reason, confidence, and facts found so far.
GET /v1/investigationsList every investigation your team has run.
POST /v1/investigations/{id}/cancelStop a running investigation early.
GET /v1/peopleEvery person your team can see, with best-per-field facts and Work Mode state, for the Database view.
GET /v1/people/{id}The canonical person record: facts (each tagged person or organisation), excluded candidates, summary.
PATCH /v1/people/{id}Team-scoped edit or override, fact corrections and Work Mode fields alike (contact status, notes). Never mutates the canonical record other teams see.
GET /v1/exportsWide, Long, CSV, JSON, or XML, filterable by minimum confidence, highest-confidence-only, subject, and observed-at date range (from and to, YYYY-MM-DD).
POST /v1/exports/recordSigned inNote an export you generated yourself in the team audit log. The in-app Download screen calls this after a download; you do not need it if you download through GET /v1/exports, which records itself.
GET /v1/usageStaffSpend this cycle against HuntPeople's own internal safety rails. Staff-only: our cost is never shown to customers, and these rails are not a customer limit.
GET /v1/analyticsAggregated dashboard series for your team: lookup activity, investigations and new people over time, confidence trends, and what kinds of fact were found. The breakdown by tier and by source is staff-only, as is cost-per-person.
GET /v1/api-keysYour team's API keys, each with its spend and optional per-key credit limit.
POST /v1/api-keysSigned inCreate an API key. Optionally set a credit limit and an expiry. The raw key is shown once, at creation.
PATCH /v1/api-keys/{id}Signed inEdit a key's name or spend limit, or disable/enable it. Disabling is reversible; the key stops working until re-enabled.
GET /v1/api-keys/{id}/activityOne key's spend and the searches that spent it.
DELETE /v1/api-keys/{id}Signed inPermanently delete a key. Its past spend stays in the credit ledger. Disable instead to pause a key you may reuse.
GET /v1/creditsYour team's credit balance and recent transactions, and roughly how many searches that buys. Credits pay for every search, from the dashboard and through a key alike.
POST /v1/credits/quoteFee math for a would-be top-up: amount, service fee, credits bought, and total due, before you buy. Send either amount_usd (what does $X buy) or credits (what does N credits cost), exactly one.
POST /v1/credits/checkoutSigned inBuy credits. Returns a URL to redirect to (Stripe payment). Self-serve, team owner only, same gate as plan billing.
POST /v1/credits/topupStaffGrant credits with no charge. Staff-only (support / comping); the self-serve path is checkout.
GET /v1/billingYour team's subscription: current plan, renewal date, seats, payment method, invoices, and the plan catalog.
GET /v1/billing/invoices/{id}Signed inOne invoice as a printable page you can save. Real Stripe invoices link to Stripe's own PDF; this covers everything else, so the download is never a dead button.
GET /v1/billing/plansNo authThe plan catalog on its own: prices, seats, monthly credits, and what each plan includes.
POST /v1/billing/checkoutSigned inSubscribe to or switch plans. Returns a URL to redirect to (Stripe Checkout).
POST /v1/billing/cancelSigned inCancel at period end, or resume a scheduled cancellation. Access is kept until the period ends.
POST /v1/billing/portalSigned inA Stripe Customer Portal URL to manage card, plan, and invoices.
PATCH /v1/billing/profileSigned inUpdate the billing email, company name, address, and tax ID shown on invoices.
GET /v1/audit-logYour team's activity: searched, viewed, edited, deleted, downloaded. Admin/cross-team visibility is a separate, developer-only scope.
GET /v1/investigations/{id}/tool-callsOne row per lookup the engine ran for an investigation: whether it found something, came back empty, or failed, and when. The evidence trail, not the user audit log. Which lookup ran is our method and is not returned.
GET /v1/admin/audit-logStaffEvery team's user-action audit trail, each row carrying its team. Developer scope.
GET /v1/teamsTeams and roles the authenticated key's account belongs to.
POST /v1/teamsSigned inCreate a team. You become its owner.
PATCH /v1/teams/{id}Signed inRename a team (owner or admin).
DELETE /v1/teams/{id}Signed inDelete a team (owner only).
POST /v1/teams/{id}/leaveSigned inLeave a team. The last owner must promote another owner or delete the team instead.
GET /v1/teams/{id}/membersSigned inMembers of a team you belong to, with their roles.
POST /v1/teams/{id}/invitesSigned inInvite someone to a team by email (owner or admin). A single-use link is sent to that address. Only an owner can invite another owner.
GET /v1/teams/{id}/invitesSigned inInvites sent for a team that nobody has accepted yet, with who sent each and when it expires.
DELETE /v1/teams/{id}/invites/{inviteId}Signed inRevoke an invite before it is accepted (owner or admin). Its link stops working.
POST /v1/teams/invites/acceptSigned inAccept an invite with its token, joining the team as the invited account.
POST /v1/teams/{id}/transfer-ownershipSigned inHand ownership of a team to another member. They become owner, you drop to admin. Owner only.
PATCH /v1/teams/{id}/members/{userId}Signed inChange a member's role (owner or admin; only an owner can manage owners). A team always keeps at least one owner.
DELETE /v1/teams/{id}/members/{userId}Signed inRemove a member from a team (owner or admin; only an owner can remove an owner).
GET /v1/notificationsSigned inYour in-app notifications (the bell), newest first, with an unread count.
POST /v1/notifications/read-allSigned inMark all your notifications read.
GET /v1/notifications/preferencesSigned inWhich notification types you have muted.
PUT /v1/notifications/preferencesSigned inReplace your muted notification types with the list you send.
GET /v1/admin/toolsStaffHuntPeople's own registry of lookups and whether each is working. Developer scope.
GET /v1/admin/integrationsStaffWhich integration keys are configured (presence only, never the secret). Developer scope.
GET /v1/admin/activityStaffEvery team's engine runs plus platform totals. Developer scope.

Two families of route are deliberately left out of the table. /v1/auth/* is the dashboard's own sign-up, sign-in, verification, and account-settings surface, driven by the session cookie rather than a key. /v1/billing/webhook is Stripe calling us, not something you call. GET /healthz returns { "ok": true } and takes no credential, for uptime checks.

The UI never commits a search without a confirm-back step first: Natural Language shows what it parsed out of your sentence, a CSV upload shows its column mapping, before hundreds of searches fire. This endpoint is that same step, parse or map without starting anything.

Parses, doesn't start anything.

curl -X POST https://api.huntpeople.ai/v1/search/preview \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"query": "Jane Doe, works at Acme, find her email"}'

Returns:

{
  "known": [{ "type": "name", "value": "Jane Doe" }, { "type": "company", "value": "Acme" }],
  "want": [{ "type": "email" }],
  "nearUniqueWarning": true
}

One call, one person.

curl -X POST https://api.huntpeople.ai/v1/search \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"email": "jane@example.com"}'

Every search costs credits at a price set before it runs: 1 for a quick search, 2 for a normal one, 7 for a deep one, and 5 to 52 for an exhaustive check depending on the model and how long you let it run. It never changes with how hard the search turned out to be. There is one balance for everything, so a search from the dashboard and a search through an API key come out of the same place. When the balance runs out, a search that would spend credits is refused with 402 until you add more or your plan renews. A search that fails on our side is never charged. Create keys and manage your credit balance together on the API keys page.

Get a result

Search is asynchronous: the POST returns an id right away and the run continues in the background, so poll this until status stops being running. status is one of running, done, failed, or cancelled. stopReason says why a finished run stopped: confidence-threshold, goals-met, diminishing-returns, budget-cap, cancelled, or error. Ids are 32-character hex strings, not prefixed.

curl https://api.huntpeople.ai/v1/investigations/9f2b6c41ad7e4f0e8b1c53d2a07e4c88 \
  -H "Authorization: Bearer sk_live_..."

Returns:

{
  "id": "9f2b6c41ad7e4f0e8b1c53d2a07e4c88",
  "status": "done",
  "stopReason": "confidence-threshold",
  "confidence": 92,
  "person": {
    "id": "c40a1e7b93d24f6ea5b8027cf1d63a19",
    "facts": [
      { "field": "email", "value": "jane@example.com", "confidence": 91, "subject": "person" }
    ]
  },
  "seeds": [{ "type": "phone", "value": "+1 704 555 0142" }],
  "wants": ["email"],
  "goals": [{ "field": "email", "status": "found", "value": "jane@example.com", "confidence": 91 }],
  "cost_usd": null,
  "createdAt": "2026-08-19T12:00:00Z",
  "sourcesChecked": 7, "signals": 4
}

Webhooks

A single search is fast enough to just poll. A batch of hundreds isn't, since completion time varies per person and polling every investigation wastes calls. Pass a callback_url on /v1/search, or once on /v1/searches/batch to cover every person in it, and HuntPeople posts to it as each investigation finishes, instead of a separate persistent webhook-subscription system to manage.

Adds one field to the same search request.

curl -X POST https://api.huntpeople.ai/v1/search \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"email": "jane@example.com", "callback_url": "https://yourapp.com/hooks/huntpeople"}'

What lands on your endpoint:

POST https://yourapp.com/hooks/huntpeople

{ "investigation_id": "9f2b6c41ad7e4f0e8b1c53d2a07e4c88", "status": "done" }

The post carries the investigation id, nothing else worth acting on. Treat it as a nudge to read GET /v1/investigations/{id}, which is where the real outcome (done or failed, stop reason, facts) lives. Delivery is best effort and unsigned, so verify by reading the investigation rather than trusting the post. Only public web addresses are called; a loopback or private address is dropped.

FAQ

Something wrong or missing on this page?