<!-- @generated by `npm run docs:gen` from src/content/docs/ — DO NOT EDIT. -->

# Search for people

Search for people with a natural-language query — or companies, by sending `target: company`.

`POST /v1/search`

Authenticated with your `x-api-key`. See [Authentication](https://ask.xverum.com/docs/authentication.md).

This endpoint searches **people** by default. To search **companies** instead, send `target: company` — see [Search for companies](#search-for-companies) below.

## Request

```http
POST /v1/search
Host: search-api.xverum.com
x-api-key: XVERUM-API-KEY
Content-Type: application/json
```

### Body

| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `query` | string | yes | 1–500 chars, trimmed, must not be blank | Natural-language search query |
| `page` | integer | no | ≥ 1, default `1` | 1-based page number |
| `page_size` | integer | no | 1–100, default `10` | Results per page |
| `target` | string | no | `people` or `company`, default `people` | Which entity to return. `company` returns companies (see [Search for companies](#search-for-companies)); no automatic people-vs-company detection is done |

### Example body

```json
{
  "query": "senior Python developers with Kubernetes in Seattle",
  "page": 1,
  "page_size": 10
}
```

## Pagination

Offset-based:

- `page` — integer, `≥ 1` (default `1`).
- `page_size` — integer, **`1` to `100`** (default `10`).
- `page * page_size` **must not exceed 1000** — the max result window.

A `page_size` outside 1–100 returns `422 validation_error`; a `page * page_size` over 1000 returns `400 invalid_pagination`. Deep pagination over the entire corpus is intentionally unsupported — refine the query instead.

`total_count` is an **estimate** of total matches, not an exact count. Use it as a rough size signal, not for precise "page N of M" math.

## Example

```bash
curl -X POST https://search-api.xverum.com/v1/search \
  -H "x-api-key: XVERUM-API-KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "senior Python developers with Kubernetes in Seattle"
  }'
```

## Response — 200 OK

| Field | Type | Description |
| --- | --- | --- |
| `result_type` | string | `people`, or `company` when you send `target: company` — discriminates the shape of `results` |
| `results` | `Profile[]` | Matched profiles (see schema below) |
| `total_count` | integer | **Estimated** total number of matches |
| `page` | integer | 1-based page number echoed back |
| `page_size` | integer | Results per page echoed back |
| `credits_used` | integer | Credits deducted on this call (1 per returned result) |
| `credits_remaining` | integer | Credits left after this call — a best-effort fresh read from billing; on a transient billing outage it falls back to the balance seen at auth time |
| `request_id` | string | Correlation id — also returned in the `X-Request-Id` header |

Every response also carries `X-Request-Id` and the `X-RateLimit-*` headers (see [Rate limits](https://ask.xverum.com/docs/rate-limits.md)).

To get the complete profile — employment history, education and profile summary — pass a result's `id` to [`GET /v1/profiles/{id}`](https://ask.xverum.com/docs/profiles.md).

### Profile schema

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Opaque profile id, stable across sessions and queries under the current scheme. Pass to [`GET /v1/profiles/{id}`](https://ask.xverum.com/docs/profiles.md) for the full profile, or to [`GET /v1/profiles/{id}/job-change`](https://ask.xverum.com/docs/predict-job-change.md) for the Next Move Signal score. Store it — any id you already hold stays fetchable. |
| `social_url` | string \| null | Person social URL; `null` when no public profile URL is on record for the person |
| `full_name` | string | Full name |
| `headline` | string \| null | Person social headline or summary; `null` when the person has not written one |
| `location` | string \| null | Location string; `null` when no location is on record for the person |
| `company_name` | string \| null | Most recent employer; `null` when no current employer is held for the person |
| `position` | string \| null | Most recent job title; `null` when no current role is held for the person |
| `industry` | string \| null | Industry vertical, taken from the person's current employer; `null` when that employer, or its industry, is not known |
| `evidence_summary` | string | How recently this profile was verified, from its refresh date — `Verified last 30 days` through `Verified over 120 days ago` |

### Example response

```json
{
  "result_type": "people",
  "results": [
    {
      "id": "a1b2c3d4",
      "social_url": "https://www.linkedin.com/in/jane-doe-sea",
      "full_name": "Jane Doe",
      "headline": "Senior Python Engineer at Acme",
      "location": "Seattle, WA",
      "company_name": "Acme",
      "position": "Senior Python Engineer",
      "industry": "software",
      "evidence_summary": "Verified last 30 days"
    },
    …
  ],
  "total_count": 128,
  "page": 1,
  "page_size": 10,
  "credits_used": 10,
  "credits_remaining": 4990,
  "request_id": "8b1d4f0c12a44b7a"
}
```

## Billing

Each profile returned in `results` consumes **1 credit** from your account, echoed back as `credits_used`; `credits_remaining` shows the balance left. A request that returns 10 profiles spends 10 credits.

`credits_remaining` is read fresh from billing on each successful call and is **best-effort**: if billing is briefly unavailable it falls back to the balance seen at authentication. Treat it as an indicative post-call balance, not a settled ledger figure.

## Search for companies

Send `target: "company"` on this same `POST /v1/search` endpoint to search companies instead of people. `query`, `page` and `page_size` behave exactly as above, and [pagination](#pagination) and the 1000 max result window are the same.

### Example body

```json
{
  "query": "fintech companies in London with 50–200 employees",
  "page": 1,
  "page_size": 10,
  "target": "company"
}
```

The response envelope is identical to the people response, except `result_type` is `company` and each item in `results` is a company card.

### Company schema

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Opaque company id. Pass to [`GET /v1/companies/{company_id}`](https://ask.xverum.com/docs/companies.md) for the full company record. Store it — any id you already hold stays fetchable. |
| `social_url` | string \| null | Company social URL; `null` when none is on record for the company |
| `company_name` | string \| null | Company name; `null` when no name is on record for the company |
| `slogan` | string \| null | Short tagline; `null` when none is on record |
| `headquarters` | string \| null | Headquarters as `City, Region`; `null` when no location is on record |
| `country_code` | string \| null | Lowercase country code of the headquarters; `null` when not known |
| `industry` | string \| null | Industry vertical; `null` when not known |
| `employees_num` | integer \| null | Employee count; `null` when not known |
| `website` | string \| null | Company website; `null` when none is on record |
| `founded` | integer \| null | Year the company was founded; `null` when not known |
| `evidence_summary` | string | How recently this company record was verified, from its refresh date — `Verified last 30 days` through `Verified over 120 days ago` |

Company results are billed exactly like people results — **1 credit per company returned**, echoed back as `credits_used`.

To get a company's full record — description, specialties, office locations, organisation type, follower count and funding — pass a result's `id` to [`GET /v1/companies/{company_id}`](https://ask.xverum.com/docs/companies.md).

## Errors

All `/v1` errors use the structured envelope (see [Authentication](https://ask.xverum.com/docs/authentication.md)):

| Status | `error.code` | Condition |
| --- | --- | --- |
| `400` | `off_topic_query` | Query is not a Xverum data query |
| `400` | `invalid_pagination` | `page * page_size` exceeds the 1000 max result window |
| `401` | `invalid_api_key` | Missing or invalid API key |
| `402` | `out_of_credits` | Out of credits — the envelope carries an `upgrade_url` to top up |
| `403` | `account_not_authorized` | Account not authorized |
| `422` | `validation_error` | Body failed validation (e.g. `query` blank or longer than 500 chars) |
| `429` | `rate_limited` | Rate limit exceeded — see [Rate limits](https://ask.xverum.com/docs/rate-limits.md) |
| `502` | `upstream_unavailable` | An upstream dependency failed — retry shortly |
| `503` | `search_unavailable` | Search is temporarily saturated or unreachable — retry shortly |
| `503` | `auth_unavailable` | Authentication is briefly unreachable — retry shortly |
| `503` | `classification_degraded` | Query understanding is degraded — retry shortly |

```json
{
  "error": {
    "code": "off_topic_query",
    "message": "Query is not a Xverum data query.",
    "request_id": "8b1d4f0c12a44b7a"
  }
}
```

The `message` is advisory and target-specific — match on `error.code`, not its text. A `target: "company"` search that is off topic returns a different message:

```json
{
  "error": {
    "code": "off_topic_query",
    "message": "Query is not a company search. If you were looking for people, retry with target=people.",
    "request_id": "8b1d4f0c12a44b7a"
  }
}
```

A company search cannot fall back to people, so this points a mis-targeted request at the right `target` rather than guessing which half to answer.
