# Constructing effective requests

> Every Nyne.ai request falls into one of two shapes. Enrichment and lookup identify one person from the identifiers you already hold; search takes a natural-language string and/or structured filters and returns the people who match. Getting the JSON shape right is what drives hit rate, latency, and credit cost.

HTML version: https://api.nyne.ai/documentation/effective-requests

The two request shapes: **enrichment / lookup** (`POST /person/enrichment`, `POST /person/lookup-fields`) identify one person, while **search** (`POST /person/search`) describes a population and returns everyone who matches.

## Person-search JSON contract

Send one JSON object. The `query` field is always a natural-language **string**, never an object or array. Put exact fields under `custom_filters`.

Valid natural-language request:

```json
{ "query": "People currently working at acme.com", "limit": 1 }
```

Valid structured request (`companies` matches current employers and accepts a company name, domain, or company profile URL):

```json
{
  "custom_filters": {
    "companies": ["acme.com"],
    "titles": ["Product Manager"]
  },
  "limit": 25
}
```

Do **not** send this shape:

```json
{ "query": { "company_domain": "acme.com" }, "limit": 1 }
```

That request confuses a natural-language field with structured filters. Use `POST /company/employees` for a broad roster at one company. Use `POST /person/search` when title, role, location, experience, or other person criteria matter.

### Agent-safe search lifecycle

1. Send each `POST /person/search` page exactly once. A new search returns `202`, `status: "processing"`, a `request_id`, and that page's `offset`/`limit`; an already-cached page can return `200` with `completed: true` immediately. POST prepares the page but does not return its result rows, and replaying a completed/cached page can charge that page again.
2. Poll and retrieve that exact page with `GET /person/search?request_id=<request_id>&offset=<offset>&limit=<limit>` using the same API credentials about every two seconds. If POST returned `completed: true`, do this GET once to read the rows without entering a polling loop. A bare GET with only `request_id` always reads offset 0; it does not remember the most recently submitted cursor.
3. Continue only while the poll returns `202` with `data.completed: false`. Stop whenever `data.completed` is `true`, regardless of whether People Search reports `status: "completed"`, `"exhausted"`, or `"stale"`. `exhausted` can coexist with `has_more: true` when later rows are already cached, so use `has_more` alone to decide whether another page exists.
4. To fetch another page, submit exactly one new POST with the prior response's `next_cursor` (recommended), or with the same `request_id` plus a higher `offset`. Record the POST response's `offset`/`limit`, then use those same values on every GET for that page. Do not use GET to launch a page fetch.
5. Stop on any error envelope or terminal `400`, `401`, `402`, `403`, `404`, `429`, or `500`. A transport-level `429` that throttles the polling call itself may be retried after `Retry-After`; a `429` stored as this search's terminal status ends that request. Retry transient `503` responses with backoff.
6. Polling is free. A POST page is billed per result returned. The base search cost and enabled options such as structured filters, contact data, insights, and scoring are additive per result. The dollar value of a credit depends on the account's subscription or credit-purchase tier.
7. An HTTPS `callback_url` can report queued session work, but it is not a per-page pagination mechanism. After a successful callback it is not re-emitted for every later page, and an immediately cached POST response does not enqueue one. Handle webhook deliveries idempotently using `request_id`, and retain the exact `request_id`/`offset`/`limit` GET for recovery and authoritative page retrieval.

## Identifiers for enrichment and lookup

Enrichment and lookup identify a single person. Send at least one of `email`, `phone`, `social_media_url`, or `name`. The more specific the identifier, the better the match: `email` and `social_media_url` resolve to exactly one person, while `name` works best when you also pass `company` and a location to disambiguate.

| key | What it is |
| --- | --- |
| email | A work or personal email address. The strongest single identifier. |
| phone | A phone number in any common format. |
| social_media_url | A profile URL, e.g. a LinkedIn page. |
| name | Full name. Works best paired with company + location to disambiguate. |
| company | Employer name. Strip legal suffixes first (see below). |
| city | City the person is in, to narrow a name match. |
| state | State or region, alongside city. |
| company_domain | Employer domain (lookup-fields), e.g. acme.com. |
| location | A free-form location string (lookup-fields). |
| postal_code | Postal or ZIP code (lookup-fields). |
| profile_url / url | A profile URL alias accepted by lookup-fields. |

Extra identifiers never hurt: each one narrows the candidate set and raises your chance of a clean, single-person match.

## Ask for only what you need: the fields array

Lookup requests take a `fields` array. Request only the fields you actually use - a narrower request is cheaper and faster, because each field can pull from a different source. Common contact fields are `best_work_email`, `best_personal_email`, and `mobile`. If you already hold specific profile URLs you want resolved, pass them in `profile_urls`.

```bash
curl -X POST https://api.nyne.ai/person/lookup-fields \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane.doe@acme.com",
    "fields": ["best_work_email", "mobile"]
  }'
```

## The email and phone shortcuts

When you need exactly one contact field, skip the `fields` array. `POST /person/email` and `POST /person/phone` are thin wrappers over lookup-fields that force the field set for you: `/person/email` forces `fields=["best_work_email"]` and `/person/phone` forces `fields=["mobile"]`, and both clear `profile_urls`. They take the same person identifiers - the simplest possible request when you only want that one value.

```bash
curl -X POST https://api.nyne.ai/person/email \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Jane Doe", "company": "acme" }'
```

```bash
curl -X POST https://api.nyne.ai/person/phone \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "social_media_url": "https://linkedin.com/in/janedoe" }'
```

Under the hood, `/person/email` is a lookup-fields call with this body forced on top of your identifiers:

```json
{
  "fields": ["best_work_email"],
  "profile_urls": []
}
```

## Start from what you know, expand outward

A hard identifier resolves to a single person on its own: an `email`, a `phone`, or a LinkedIn or other `social_media_url`. Once you have one, submit it back to enrichment to expand the record - filling out title, company, socials, and work history from that single anchor. You do not need to send anything else:

```bash
curl -X POST https://api.nyne.ai/person/enrichment \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "social_media_url": "https://linkedin.com/in/janedoe" }'
```

This "known key in, full record out" flow is the backbone of most integrations: capture whatever strong identifier a user gives you, then let enrichment fill in the rest.

## Company-name hygiene

Legal and entity suffixes throw off company matching. Before you send a `company` value, lowercase it and strip trailing suffix tokens repeatedly until none remain. So `"Acme Corp Inc"` becomes `"acme"`. Copy-paste this suffix list:

```
inc, llc, ltd, co, corp, corporation, company, gmbh, sa, ag,
plc, llp, lp, pllc, pty, group, holdings, holding, the
```

Match tokens case-insensitively and remove them one at a time from the end, so a doubled suffix like `Corp Inc` is fully cleaned. Do this to any name you send as `company` - in enrichment, lookup, the shortcuts, and the search `companies` filter alike.

## Search filters and requirements

Search does not identify one person. It takes a natural-language `query` string, a `custom_filters` object, or both, and returns everyone who matches. Most fields in `custom_filters` are arrays you can combine, alongside numeric ranges and bounded structured objects:

| custom_filters field | Matches on |
| --- | --- |
| locations | Regions, metros, or countries. |
| languages | Languages the person uses. |
| titles | Job titles or role names. |
| industries | Industry categories. |
| companies | Current employers; accepts names, domains, and company profile URLs. |
| universities | Schools attended. |
| keywords | Free-text terms matched across the profile. |
| degrees | Degree types held. |
| specialization_categories | Areas of specialization. |
| company_funding_stages | Funding stage of the employer. |
| company_latest_funding_stages | Most recent funding stage. |
| company_investor_names | Investors backing the employer. |
| company_funding_round_filters | Same-event employer funding constraints. Each object may contain `stages`, `investor_names`, `min_amount`, `max_amount`, `funded_after_date`, `funded_before_date`, and `funded_within_days`. |

Use `company_funding_round_filters` when facts must come from one round. For example, `{"stages":["series a"],"min_amount":3000000,"funded_within_days":30}` requires a Series A round of at least $3 million within the inclusive last 30 days; it does not allow the stage, amount, and date to come from separate rounds.

Alongside the filters, contact-requirement toggles narrow the result set to people who have the contact data you care about, and `profile_scoring` ranks each result against the query:

| parameter | Effect |
| --- | --- |
| require_emails | Keep only results that have an email. |
| require_phone_numbers | Keep only results that have a phone. |
| require_phones_or_emails | Keep only results with a phone or an email. |
| profile_scoring | Attach a query-fit score to each result. |

```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 '{
    "custom_filters": {
      "titles": ["VP of Engineering", "CTO"],
      "locations": ["San Francisco Bay Area"],
      "industries": ["Software"],
      "min_linkedin_followers": 1000
    },
    "require_phones_or_emails": true,
    "profile_scoring": true,
    "limit": 10
  }'
```

Each requirement you add filters more people out and can raise the cost per result, because guaranteeing a phone or email means resolving more data. See [Credits](https://api.nyne.ai/documentation/credits.md) and [Request depth](https://api.nyne.ai/documentation/request-depth.md) for how the trade-off is priced.

## Async delivery with callback_url

Any submit can carry a `callback_url`. When the result is ready, Nyne POSTs it there instead of making you poll. The URL is validated against an allowed-hosts list, so register your host first. See [Async and webhooks](https://api.nyne.ai/documentation/async.md) for the full setup.

## What's next

- [Request depth](https://api.nyne.ai/documentation/request-depth.md) - how far Nyne digs for a result, and what that costs.
- [Credits](https://api.nyne.ai/documentation/credits.md) - what each request type charges, and when.
- [Async and webhooks](https://api.nyne.ai/documentation/async.md) - polling versus `callback_url` delivery.
- [Responses](https://api.nyne.ai/documentation/responses.md) - the envelope and the fields you get back.
