Skip to main content
Person Search accepts a plain-English audience description (“CFOs at Series B fintech startups in NYC”) and returns the matching Minerva PIDs plus coverage statistics for how many of them have email, phone, LinkedIn, etc.
from minerva import Minerva

mc = Minerva()

# 1. Kick off a search
resp = mc.api.person_search(
    query="CFOs at Series B fintech startups in NYC",
    size=100,
)
resp.search_id                                  # str — store to re-fetch later
resp.results                                    # list[str] of Minerva PIDs
resp.total_count                                # full match count (may exceed size)
resp.total_contact_coverage.professional_email  # CoverageStat(count, percent)

# 2. Fetch the same search later by id
again = mc.api.person_search_get(resp.search_id)

# 3. Check your per-day usage
usage = mc.api.person_search_usage()
usage.current_day_remaining                     # remaining records in today's quota

Sync-only by design — no AsyncMinerva support

AsyncMinerva.api.person_search (and its _get / _usage siblings) raise NotImplementedError. The endpoint runs for several seconds per call server-side and is metered per UTC day, so allowing concurrent fan-out via asyncio.gather(...) would make it trivial to drain the daily quota in a single client-side mistake. Use the sync Minerva client.
async with AsyncMinerva() as mc:
    await mc.api.person_search("...")
    # raises NotImplementedError: AsyncMinerva.api.person_search is not
    # supported — person-search is sync-only by design. Use Minerva().

Validation (before any HTTP call)

The wrapper validates locally first, so customer-side bugs surface as MinervaValidationError without burning a quota credit:
  • query must be 1–500 characters
  • size must be in 1–100
mc.api.person_search("", dry_run=True)
# MinervaValidationError: query — String should have at least 1 character
Pass dry_run=True to validate + get back the PersonSearchRequest model without firing the call.

Retrying transient errors

Person Search is one of Minerva’s slowest endpoints — it makes its own upstream LLM calls to parse your query, which can take several seconds and is more exposed to transient network drops than the other data-plane methods. The SDK does not auto-retry, but it classifies network drops, read/connect timeouts, and 5xx responses as MinervaTransientError — a subclass of MinervaAPIError — so you can write a tight retry loop without catching things you don’t want to retry (auth failures, input validation, etc.). The recommended pattern uses tenacity:
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type,
)
from minerva import Minerva, MinervaTransientError

mc = Minerva()

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=1, max=10),
    retry=retry_if_exception_type(MinervaTransientError),
)
def search_with_retry(query: str, size: int = 100):
    return mc.api.person_search(query, size=size)

resp = search_with_retry("CFOs at Series B fintech startups in NYC")
MinervaTransientError covers:
  • Connection drops mid-response (the RemoteDisconnected family)
  • Connect / read / write timeouts
  • All 5xx HTTP responses
It does not cover (correctly, since retrying makes no difference):
  • MinervaValidationError — local input validation failed
  • MinervaAuthError401/403 API-key issue
  • MinervaRateLimitError429, use the retry_after field on the exception to back off, then retry; don’t bury it in a generic retry loop
  • Other MinervaAPIError subclasses for 4xx responses — your input is the problem

Writing good queries

See the API reference for the canonical guide: Writing good queries → The page documents:
  • Supported attribute categories (role, industry, company, career history, location, education, age, income, contact channels)
  • Three worked examples with progressively more complexity
  • Tips on how to phrase queries the parser handles well

Feeding results into other SDK methods

Person Search returns PIDs. Plug them straight into mc.api.enrich to get full profiles for the matched audience — no second resolve hop needed:
search = mc.api.person_search("Marketing directors at SaaS in the Bay Area")
profiles = mc.api.enrich(
    [{"record_id": pid, "minerva_pid": pid} for pid in search.results],
)
This is the fastest path from “describe an audience” → “have their profile data” — the enrich call is keyed by PID, so it skips the match pipeline server-side (same optimization as mc.workflows.resolve_then_enrich).

Full schema

The request body fields (query, size), response shape (search_id, results, total_count, contact-coverage stats), and error responses are documented under the API Reference: