# Person Search

> Find people by natural-language query with structured filters.

- **Endpoint:** `POST https://api.nyne.ai/person/search`
- **Group:** Person APIs (https://api.nyne.ai/documentation/person.md)
- **Auth:** `X-API-Key` + `X-API-Secret` headers
- **Mode:** Asynchronous - submit returns a `request_id` (normally with `202 Accepted`); poll `GET /person/search?request_id=<request_id>&offset=<offset>&limit=<limit>` with the same auth headers until the endpoint reports a terminal response. Where supported, a `callback_url` can notify you when queued work finishes; follow this endpoint's retrieval notes.
- **HTML version:** https://api.nyne.ai/documentation/person/search

Send one JSON object. `query`, when present, must be a natural-language JSON string, never an object or array. Put exact fields under `custom_filters`. For example, send `{"query":"People currently working at acme.com","limit":1}`, or a filter-only request such as `{"custom_filters":{"companies":["acme.com"]},"limit":25}`. Do not send `{"query":{"company_domain":"acme.com"}}`. Submit each page once. A new search returns `202` plus a `request_id`; a page already cached for that request can return `200` immediately. The POST prepares the page but does not return its result rows. Read the page with `GET /person/search` using that `request_id` and the page's `offset`/`limit`; if POST returned `200`, issue that GET once without entering a polling loop. A processing poll is `202` with `data.completed: false`; a successful terminal poll is `200` with `data.completed: true`. Stop polling whenever `completed` is true, regardless of whether `status` is `completed`, `exhausted`, or `stale`. A bare GET with only `request_id` always reads offset 0; it does not remember the most recently submitted cursor. Polling is free. `callback_url` can notify you after queued session work, but it is not a per-page pagination mechanism: a successful callback is not re-emitted for every later page, and an immediate cached `200` does not enqueue one. Always retain the exact GET retrieval path. Use `/company/employees` for a broad company roster and `/person/search` when role, title, location, or other person criteria matter.

## Authentication

All `/person/*` and `/company/*` endpoints authenticate with an API key and secret sent as HTTP headers on every request: `X-API-Key` and `X-API-Secret`. Create keys from your Nyne.ai dashboard and keep the secret server-side. Rate limits: 100 requests/minute, 1,000 requests/hour. Full guide: https://api.nyne.ai/documentation/authentication.md

## Parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| `query` | string | no | Natural-language JSON string describing who you're looking for (max 1000 chars). It must not be an object, array, or search DSL. Provide a `query` and/or structured `custom_filters` - at least one is required unless paginating with `cursor` or `request_id`. With no `query`, the search runs purely on `custom_filters`. | "Marketing directors at fintech startups in Austin" |
| `limit` | integer | no | Results per page. Default 10; valid values are 1-100. Larger result sets are retrieved by paginating (see Pagination). Values outside the range return HTTP 400. | 10 |
| `show_emails` | boolean | no | Include verified email addresses in each result. | true |
| `show_phone_numbers` | boolean | no | Include phone numbers in each result. | true |
| `require_emails` | boolean | no | Only return people for whom a verified email could be found. | true |
| `require_phone_numbers` | boolean | no | Only return people for whom a phone number could be found. | true |
| `require_phones_or_emails` | boolean | no | Only return people with at least one contact method (email or phone). | true |
| `insights` | boolean | no | Attach AI-generated relevance insights per result. When present, `insights.why_matched` contains concise evidence chips showing the criterion, evidence type, confidence, and a safe short matched phrase when available. Most useful when a `query` is supplied. | true |
| `profile_scoring` | boolean | no | Return a query-fit `score` (integer 1-5) ranking how well each profile matches the query. Most useful when a `query` is supplied. | true |
| `custom_filters` | object | no | Structured filters - `locations`, `titles`, `companies`, `universities`, `degrees`, `specialization_categories`, plus the array filters `industries` (industry names, lowercase; common taxonomy synonyms accepted, e.g. `["fintech","financial services"]`), `keywords` (free-text terms that must each appear in the profile text/skills/interests; multiple keywords are ANDed), and `languages` (spoken languages, 2-3 common forms each, e.g. `["spanish","espanol"]` or `["chinese","mandarin","中文"]`). `companies` matches current employers and accepts company names, domains such as `acme.com`, or company profile URLs. Employer-company filters include `min_company_employee_count`/`max_company_employee_count`, `company_employee_count_scope`, `min_company_total_funding_raised`/`max_company_total_funding_raised`, `company_funding_stages`, `company_latest_funding_stages`, `company_investor_names`, `company_last_funding_after_date`/`company_last_funding_before_date`, `company_funding_round_filters`, and `company_funding_scope`. Each `company_funding_round_filters` item binds its supplied `stages`, `investor_names`, `min_amount`/`max_amount`, `funded_after_date`/`funded_before_date`, and `funded_within_days` to the same funding event. Supports array filters (any-of), numeric ranges, booleans, and exact-match values. | { "locations": ["NYC"], "company_funding_round_filters": [{ "stages": ["series a"], "min_amount": 3000000, "funded_within_days": 30 }], "company_funding_scope": "current" } |
| `cursor` | string | no | Opaque pagination token from a previous response's `next_cursor`. Submit it in a new `POST /person/search` to fetch that page. Preserve the `offset` and `limit` returned by this POST and include them on the GET that polls/retrieves the page. | "eyJvIjo1MCwi…" |
| `offset` | integer | no | Starting position for offset pagination (0-indexed). Default 0; `offset` + `limit` may not exceed 10000. | 0 |
| `request_id` | string | no | ID of an existing search (returned by the first call). Re-send it in a `POST` with a new `offset`/`limit` to fetch another page. Use `GET` with that same `request_id`, `offset`, and `limit` to poll/retrieve the requested page. A bare GET does not launch a page fetch; it reads offset 0. Polling never consumes credits. | "abc12345_1737123456_1234" |
| `callback_url` | string | no | HTTPS URL for a session callback after queued search work. This is not a per-page pagination mechanism: after a successful callback it is not re-emitted for every later page, and an immediately cached page does not enqueue one. Process callbacks idempotently by request_id, and retain the exact request_id/offset/limit GET for recovery and authoritative page retrieval. | "https://example.com/webhooks/nyne" |

## Polling for the result

A newly queued submit normally returns `202 Accepted` with a `request_id`. For People Search, `data.completed` is authoritative for successful responses: keep polling only while it is `false`, and stop whenever it is `true` regardless of the successful `status` value. Any terminal error envelope or non-retriable non-2xx job response also stops that request; transient transport/status-infrastructure responses such as `503` may be retried with backoff. Where supported, pass a `callback_url` on the submit to receive a webhook when queued work finishes; keep the documented GET as a recovery and retrieval path:

```bash
curl "https://api.nyne.ai/person/search?request_id=<request_id>&offset=<offset>&limit=<limit>" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET"
```

| Status | Meaning |
| --- | --- |
| processing | HTTP 202 with `completed: false`. The requested page is still being prepared; keep polling. |
| pending / enriching | HTTP 202 with `completed: false`. These non-terminal values can appear only when polling an older request created by the legacy search implementation; keep polling. |
| completed | HTTP 200 with `completed: true`. The requested page is ready. Use `has_more` to decide whether to request another page. |
| exhausted | HTTP 200 with `completed: true`. The search source cannot add more matches. This can still have `has_more: true` when later rows are already stored in the session; `has_more`, not this status word, decides pagination. |
| stale | HTTP 200 with `completed: true`. Cached results are older than 30 days; the response includes a warning and remains a terminal success. |

## Pagination

Each page has two phases: submit exactly one `POST` to fetch/prepare that page, then use `GET` to poll and read that exact slice. POST returns page metadata, not result rows. If POST immediately returns `200` with `completed: true`, skip repeated polling but still issue one GET for the rows. For pages after the first, include the `offset` and `limit` from the page-submit response on every GET. A GET with only `request_id` defaults to offset 0 and therefore re-reads page 1; it neither remembers a cursor POST nor launches a new page fetch. Each successfully fulfilled page POST is billed for results on that page, and replaying an already completed/cached page can charge that page again; GET polling/retrieval is free. `offset + limit` may not exceed 10000.

### Knowing when to paginate

| Response field | Meaning |
| --- | --- |
| completed | The definitive polling signal. `false` means keep polling; `true` means stop polling and consume this page, regardless of whether `status` is `completed`, `exhausted`, or `stale`. |
| status | `processing` is non-terminal. `completed`, `exhausted`, and `stale` are successful terminal states. `exhausted` describes upstream fetchability, not whether the current slice has a cached next page. |
| has_more | `true` means more results are available beyond this page - fetch the next page. `false` means you have reached the end; stop. |
| next_cursor | An opaque token for the next page. Present only when `has_more` is `true` (omitted on the last page). Pass it back as the `cursor` parameter - it already encodes the next `offset`, `limit` and `request_id`. |
| total_estimate | Approximate total number of matches for the query. Use it to size a progress bar or decide how many pages to pull; treat it as an estimate, not an exact count. |
| credits_charged | Credits recorded on this search session's fetch activity. It is not a charge for the current GET, and it is not the authoritative account billing ledger; polling and stored-page reads are free. Use `GET /usage` for account-level consumption. |
| offset / limit | The slice this response represents: results `offset` through `offset + limit - 1`. |

### Fetching the next page

| Method | How |
| --- | --- |
| Cursor (recommended) | Take `next_cursor` from the response and submit `POST /person/search` exactly once with `{"cursor":"..."}` (no other body fields needed). Record the POST response's `request_id`, `offset`, and `limit`. Poll/read that page with `GET /person/search?request_id=...&offset=...&limit=...`. If POST already returned `completed: true`, perform the GET once to read the rows. Repeat the page cycle only while the GET response has `has_more: true`. |
| request_id + offset | Submit the original `request_id` in a new POST with an increasing `offset` and the chosen `limit`. Then poll/read with a GET carrying the same `request_id`, `offset`, and `limit`. |
| Repeat the query + offset | Re-submit the same `query` and identical search options with a higher `offset`. The service matches it to the existing search session. Poll/read with the returned `request_id` and that same `offset`/`limit`. Prefer cursor or request_id when available because they are unambiguous. |

Do not use a bare `GET ?request_id=...` to retrieve a page submitted at a nonzero offset: the GET defaults to offset 0 and will correctly return page 1 with the same first-page cursor. Do not resubmit POST as a status check: an identical POST can become another billable page request. Carry the page's `offset`/`limit` through the complete POST-to-GET lifecycle. Results retain stable positions within the session, and re-reading stored data with GET is free.

## Credit usage

| Item | Credits | Condition |
| --- | --- | --- |
| Person Search | 1 | base credits per result returned |
| Smart Ranking | +1 | per result when profile_scoring: true |
| Candidate Insights | +1 | per result when insights: true |
| Structured Filters | +1 | per result when custom_filters is non-empty |
| Email Data | +6 | per result when show_emails or require_emails is true |
| Phone Data | +6 | per result when show_phone_numbers or require_phone_numbers is true |
| Any Contact Data | +6 | per result when require_phones_or_emails is true and no email/phone add-on is already enabled |

## Responses

| Code | Meaning |
| --- | --- |
| `202` | Search queued or still processing - poll with the returned request_id |
| `200` | Requested page is ready; successful terminal statuses are completed, exhausted, and stale, all with completed: true |
| `400` | Invalid parameters, including an object-valued query or missing search scope |
| `401` | Missing, invalid, or expired API credentials |
| `402` | Insufficient credits |
| `403` | Valid credentials, but the account, plan, product, subscription, or request IP is not allowed |
| `404` | Unknown request_id or a request_id not owned by this API key |
| `429` | Rate or monthly request limit exceeded |
| `500` | The queued search reached a terminal processing error; do not keep polling that request_id |
| `503` | Search or status infrastructure is temporarily unavailable; retry with backoff |

## Example request

### cURL

```bash
curl -X POST https://api.nyne.ai/person/search \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Sales managers at Fortune 500 companies in San Francisco",
    "limit": 10,
    "show_emails": true,
    "show_phone_numbers": true,
    "profile_scoring": true,
    "insights": true,
    "custom_filters": {
      "min_company_employee_count": 500,
      "company_investor_names": [
        "Sequoia Capital"
      ],
      "min_company_total_funding_raised": 10000000,
      "company_funding_scope": "current"
    }
  }'
```

### Python

```python
import requests

resp = requests.post(
    "https://api.nyne.ai/person/search",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "X-API-Secret": "YOUR_API_SECRET",
    },
    json={
        "query": "Sales managers at Fortune 500 companies in San Francisco",
        "limit": 10,
        "show_emails": True,
        "show_phone_numbers": True,
        "profile_scoring": True,
        "insights": True,
        "custom_filters": {
            "min_company_employee_count": 500,
            "company_investor_names": [
                "Sequoia Capital",
            ],
            "min_company_total_funding_raised": 10000000,
            "company_funding_scope": "current",
        },
    },
)
data = resp.json()
print(data)
```

### Node

```javascript
const resp = await fetch("https://api.nyne.ai/person/search", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "X-API-Secret": "YOUR_API_SECRET",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    query: "Sales managers at Fortune 500 companies in San Francisco",
    limit: 10,
    show_emails: true,
    show_phone_numbers: true,
    profile_scoring: true,
    insights: true,
    custom_filters: {
      min_company_employee_count: 500,
      company_investor_names: [
        "Sequoia Capital",
      ],
      min_company_total_funding_raised: 10000000,
      company_funding_scope: "current",
    },
  }),
});
const data = await resp.json();
console.log(data);
```

### PHP

```php
<?php
$ch = curl_init("https://api.nyne.ai/person/search");
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    "X-API-Key: YOUR_API_KEY",
    "X-API-Secret: YOUR_API_SECRET",
    "Content-Type: application/json",
  ],
  CURLOPT_POSTFIELDS => json_encode([
    "query" => "Sales managers at Fortune 500 companies in San Francisco",
    "limit" => 10,
    "show_emails" => true,
    "show_phone_numbers" => true,
    "profile_scoring" => true,
    "insights" => true,
    "custom_filters" => [
      "min_company_employee_count" => 500,
      "company_investor_names" => [
        "Sequoia Capital",
      ],
      "min_company_total_funding_raised" => 10000000,
      "company_funding_scope" => "current",
    ],
  ]),
]);
$data = json_decode(curl_exec($ch), true);
print_r($data);
```

## Initial 202 response

```json
{
  "request_id": "abc12345_1737123456_1234",
  "status": "processing",
  "completed": false,
  "message": "Search request is being processed. Poll GET /person/search with request_id to check status.",
  "offset": 0,
  "limit": 10
}
```

## Completed poll response

```json
{
  "request_id": "abc12345_1737123456_1234",
  "status": "completed",
  "completed": true,
  "results": [
    {
      "profile_id": "johnsmith",
      "displayname": "John Smith",
      "firstname": "John",
      "middlename": "A",
      "lastname": "Smith",
      "gender": "male",
      "headline": "Senior Sales Manager at Tech Corp",
      "bio": "Enterprise sales leader with 15 years closing Fortune 500 accounts.",
      "location": "San Francisco, CA",
      "altemails": [
        "john.smith@techcorp.com",
        "john.smith@gmail.com"
      ],
      "fullphone": [
        {
          "fullphone": "+1-555-123-4567",
          "phone_type": "mobile"
        }
      ],
      "social_profiles": {
        "linkedin": {
          "url": "https://linkedin.com/in/johnsmith",
          "username": "johnsmith",
          "followers": 1200,
          "connections": 500
        }
      },
      "organizations": [
        {
          "name": "Tech Corp",
          "company_domain": "techcorp.com",
          "company_employee_count": 1200,
          "company_employee_count_range_start": 1001,
          "company_employee_count_range_end": 5000,
          "company_total_funding_raised": 25000000,
          "company_latest_funding_stage": "series b",
          "company_funding_stages": [
            "seed",
            "series a",
            "series b"
          ],
          "company_funding_rounds": [
            {
              "stage": "series b",
              "date": "2025-10-03",
              "amount": 15000000,
              "investor_names": [
                "sequoia capital"
              ]
            }
          ],
          "company_last_funding_date": "2025-10-03",
          "company_investor_names": [
            "sequoia capital"
          ],
          "title": "Senior Sales Manager",
          "startDate": "2020-03",
          "endDate": null
        }
      ],
      "schools_info": [
        {
          "name": "University of California, Berkeley",
          "degree": "BS",
          "specialization_category": "Business",
          "startDate": "2001",
          "endDate": "2005"
        }
      ],
      "skills": [
        "enterprise sales",
        "negotiation"
      ],
      "languages": [
        "english"
      ],
      "estimated_age": 42,
      "score": 5,
      "insights": {
        "overall_summary": "Strong match: senior sales leadership at a large enterprise in the target metro.",
        "why_matched": [
          {
            "criterion": "Sales managers at Fortune 500 companies",
            "evidence_type": "work_experience",
            "confidence": "strong",
            "display_text": "Current role shows senior sales management at Tech Corp.",
            "matched_phrase": "Senior Sales Manager at Tech Corp"
          },
          {
            "criterion": "San Francisco",
            "evidence_type": "location",
            "confidence": "strong",
            "display_text": "Profile location is in the requested metro."
          }
        ],
        "query_insights": [
          {
            "subquery_idx": 0,
            "subquery": "Sales managers at Fortune 500 companies",
            "priority": "Essential",
            "match_level": "Meets Expectations",
            "short_rationale": "Senior Sales Manager at a Fortune 500 company.",
            "rationale": "Currently Senior Sales Manager at Tech Corp, a Fortune 500 enterprise.",
            "short_quotes": [
              "Senior Sales Manager at Tech Corp"
            ]
          }
        ]
      }
    }
  ],
  "offset": 0,
  "limit": 10,
  "total_estimate": 500,
  "has_more": true,
  "next_cursor": "eyJvIjoxMCwiciI6ImFiYzEyMzQ1…",
  "credits_charged": 160
}
```

---

All documentation pages are available as Markdown by appending `.md` to their URL. Index: https://api.nyne.ai/llms.txt
