# Migrating people search from persons to people

> Move people search calls from /v2/search/persons to /v2/search/people.

People search has a new home: `POST /v2/search/people`. It returns full profiles instead of search hits, paginates with cursors instead of page numbers, accepts a larger filter set, and ships with a free count endpoint.

The previous endpoint, `POST /v2/search/persons`, remains functional until **October 13, 2026**, when it is retired. See the [endpoint transition overview](https://docs.reversecontact.com/guides/endpoint-transition-october-2026) for the full timeline. It is no longer documented in the endpoint catalog, and this guide covers everything that changes when you move to `/v2/search/people`.

## What changes

| Area | `POST /v2/search/persons` | `POST /v2/search/people` |
| ---- | ------------------------- | ------------------------ |
| Path | `/v2/search/persons` | `/v2/search/people` |
| Results | Search hits (name, title, company, location) | Full profiles, the same shape as the [Profile](https://docs.reversecontact.com/endpoints/fetch-profile) endpoint |
| Pagination | `page` and `perPage` (default `100`) | `cursor` and `perPage` (default `25`) |
| Metadata | `currentPage`, `pageNumber`, `perPage`, `total` | `perPage`, `count`, `nextCursor` |
| Total matches | `data.metadata.total` on every response | Free [count endpoint](https://docs.reversecontact.com/endpoints/search-people-count) |
| Pricing | 1 credit per page | 1 credit per profile delivered |
| Available fields | `GET /v2/search/persons/available-fields` | `GET /v2/search/people/available-fields` |

## Paths

| Before | After |
| ------ | ----- |
| `POST /v2/search/persons` | `POST /v2/search/people` |
| `GET /v2/search/persons/available-fields` | `GET /v2/search/people/available-fields` |
| (no equivalent) | `POST /v2/search/people/count` |

The method and the `Authorization: Bearer YOUR_API_KEY` header stay the same. Only the path changes.

## Results are full profiles

The previous endpoint returned a light search hit, and the documented flow was to chain into the [Profile](https://docs.reversecontact.com/endpoints/fetch-profile) endpoint for the full data of a hit. People search returns complete profiles directly, so the second call is no longer needed.

Before, a hit looked like this:

```json
{
  "id": "prs_01h2x3y4z5a6b7c8d9e0f1g2h3",
  "publicId": "janedoe",
  "linkedinUrl": "https://www.social.com/in/janedoe",
  "firstName": "Jane",
  "lastName": "Doe",
  "headline": "VP of Marketing @ Acme Corp",
  "currentPositionTitle": "VP of Marketing",
  "currentCompanyName": "Acme Corp",
  "updateDate": "2026-03-18T09:12:44.000Z"
}
```

After, each item of `data.data` is a full profile, with `currentPosition`, `experience`, `education`, `skills` and the other sections documented on the [Profile](https://docs.reversecontact.com/endpoints/fetch-profile) endpoint. If you chained Profile calls after a search, drop them: the data is already in the search response.

## Pagination moves from pages to cursors

The previous endpoint used `page` (1 to 50) and `perPage` (1 to 100, default `100`), and reported `currentPage`, `pageNumber` and `total` in `data.metadata`. People search uses cursor pagination:

- `perPage` stays, with a new default of `25` (still 1 to 100).
- `page` is gone. Send the `nextCursor` value from the previous response as `cursor` to get the next page.
- `nextCursor` is `null` on the last page.
- The cursor is opaque: send it back unchanged, without parsing or altering it.

Before:

```javascript
const response = await fetch("https://api.reversecontact.com/v2/search/persons", {
  method: "POST",
  headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ currentPositionTitle: "VP of Marketing", page: 2, perPage: 100 })
});

const payload = await response.json();
const hits = payload.data.data;
console.log(`Page ${payload.data.metadata.currentPage} of ${payload.data.metadata.pageNumber}, ${payload.data.metadata.total} total`);
```

After:

```javascript
const searchPage = async (cursor) => {
  const response = await fetch("https://api.reversecontact.com/v2/search/people", {
    method: "POST",
    headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
    body: JSON.stringify({ currentSeniority: "VP", perPage: 25, cursor })
  });

  return response.json();
};

const page = await searchPage();
console.log(`${page.data.metadata.count} profiles in this page`);
if (page.data.metadata.nextCursor) {
  const nextPage = await searchPage(page.data.metadata.nextCursor);
}
```

## Totals move to the count endpoint

The previous `data.metadata.total` field is gone: `data.metadata.count` is the number of profiles in the current page, not the size of the whole matching set. To size a query, call the free [People search count](https://docs.reversecontact.com/endpoints/search-people-count) endpoint with the same filters:

```bash
curl -X POST https://api.reversecontact.com/v2/search/people/count \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "currentSeniority": "VP" }'
```

The response is `data.count`, the total number of matching profiles across all pages, and it costs 0 credits.

## Pricing changes

The previous endpoint charged 1 credit per page, whatever the page contained. People search charges **1 credit per profile delivered**: a page that returns 25 profiles costs 25 credits, and a page that returns no profile costs 0 credits. Rejected filters still cost 0 credits.

Two consequences for your migration:

- `perPage` is now a credit control. Lower it to cap what a single request can spend.
- Size queries first with the free count endpoint instead of reading `metadata.total` from a billed page.

## Filter changes

Existing filters keep their names, including `currentPositionTitle`, `excludeCurrentPositionTitle`, `currentCompanyIndustry`, `currentCompanyEmployeeCountRange`, `location`, `followersCount` and `maxDataAgeDate`. People search also accepts filters the previous endpoint did not have:

| Filter | What it does |
| ------ | ------------ |
| `currentFunction`, `excludeCurrentFunction` | Match the current function from a 32-value position taxonomy |
| `currentSubFunction`, `excludeCurrentSubFunction` | Match the current sub-function from a 386-value taxonomy |
| `currentSeniority`, `excludeCurrentSeniority` | Match the current seniority from a 10-value taxonomy |
| `experience` | Require past experience in a function, sub-function or seniority, with an optional minimum tenure in months |
| `positionTitle`, `companyName`, `companyId`, `companyLinkedinId`, `companyPublicId`, `companyIndustry` and their `exclude...` counterparts | Match current and past positions, not only the current one |
| `excludeCurrentCompanyName`, `excludeCurrentCompanyId`, `excludeCurrentCompanyLinkedinId`, `excludeCurrentCompanyPublicId` | Exclude a specific current employer |
| `email` | Filter by email address |
| `isCurrentlyEmployed` | Restrict to profiles that have a current employer |
| `skills` | Match the skills listed on the profile |
| `languages`, `excludeLanguages` | Match or exclude the languages a person speaks |
| `keywords` | Require every word somewhere in the profile, in any field |
| `changedJobsWithinDays` | Keep people who started their current position within the last 30, 60, 90 or 120 days |

The taxonomy lists are documented on the [People search](https://docs.reversecontact.com/endpoints/search-people#position-taxonomy) page and returned dynamically by [available fields](https://docs.reversecontact.com/endpoints/search-people-available-fields). Validation is unchanged in spirit: unknown fields, blank values and values outside a closed list return `422` with suggestions and cost 0 credits.

## Available fields moves too

If you discover filters dynamically, switch `GET /v2/search/persons/available-fields` to `GET /v2/search/people/available-fields`. The response shape is unchanged: `data.entity` is `"person"` and `data.fields` lists every accepted filter, with `allowedValues` for closed vocabularies such as the position taxonomy.

## Migration checklist

1. Replace the search path with `POST /v2/search/people` and the available-fields path with `GET /v2/search/people/available-fields`.
2. Replace `page` with cursor handling: loop while `data.metadata.nextCursor` is not `null`.
3. Review your `perPage`: the default is now `25`, and each delivered profile costs 1 credit.
4. Replace `data.metadata.total` reads with a call to `POST /v2/search/people/count`.
5. Drop the Profile calls you used to chain after a search: results are already full profiles.
6. Map your title filters: `currentPositionTitle` still matches the current position, and the new `positionTitle` matches current and past positions.
