# Search with filters

> POST /v1/people/search. An exact filter set in, a deterministic page of people out, one credit a call. Every field, every allowed value, and how to paginate.

Canonical page: https://www.refolk.ai/developers/structured-search



An exact filter set in, a page of people out. No model runs and nothing is
ranked, so the same filters return the same page tomorrow, which is what makes
this the one to build a pipeline on.

### `POST /api/v1/people/search`

Deterministic, repeatable, cursor-paginated. A filter set in, a page of people out.

**Cost:** 1 credit

## The request

**Body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | `object` | required | The filter set. Every field is optional and at least one is required. |
| `limit` | `integer` | optional | How many to return, 1 to 100. Defaults to 25. |
| `cursor` | `string` | optional | The `nextCursor` from a previous response. |

**curl**

```bash
curl https://www.refolk.ai/api/v1/people/search \
  -H "Authorization: Bearer $REFOLK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": {
      "titleIncludes": ["engineer"],
      "skills": ["Rust"],
      "countries": ["Germany"],
      "seniorityLevels": ["Senior", "Director"],
      "minYearsExperience": 6
    },
    "limit": 25
  }'
```

**TypeScript**

```typescript
const res = await fetch("https://www.refolk.ai/api/v1/people/search", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.REFOLK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    query: {
      titleIncludes: ["engineer"],
      skills: ["Rust"],
      countries: ["Germany"],
      seniorityLevels: ["Senior", "Director"],
      minYearsExperience: 6,
    },
    limit: 25,
  }),
});
const { profiles, totalCount, nextCursor } = await res.json();
```

**Python**

```python
res = requests.post(
    "https://www.refolk.ai/api/v1/people/search",
    headers={"Authorization": f"Bearer {os.environ['REFOLK_API_KEY']}"},
    json={
        "query": {
            "titleIncludes": ["engineer"],
            "skills": ["Rust"],
            "countries": ["Germany"],
            "seniorityLevels": ["Senior", "Director"],
            "minYearsExperience": 6,
        },
        "limit": 25,
    },
)
```

Filters are combined with AND: the query above means senior or director-level
engineers, in Germany, with Rust on file, and at least six years in.

Run this request in the playground: https://www.refolk.ai/developers/playground?endpoint=people-search. One credit. Returns in a second or two.

```json
{"query": {"titleIncludes": ["engineer"], "skills": ["Rust"], "countries": ["Germany"]}, "limit": 5}
```

> **An empty filter set is refused**
>
> `query: {}` is a `400`, not a search for everybody. Upstream would happily
> answer "every person" and charge for it, and nobody has ever meant that.

## Every filter

**query**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `keywords` | `string` | optional | Free text, matched against the headline. |
| `titleIncludes` | `string[]` | optional | Job titles to include, matched as substrings. `"engineer"` catches every kind of engineer. |
| `titleExcludes` | `string[]` | optional | Job titles to exclude. |
| `companyNames` | `string[]` | optional | Employers, by their common name rather than their legal one. |
| `companyIndustries` | `string[]` | optional | The industry the employer is in. |
| `seniorityLevels` | `string[]` | optional | One or more of `Owner`, `Founder`, `CXO`, `Partner`, `VP`, `Director`, `Manager`, `Senior`, `Entry`. Anything else is a `400`. |
| `headcountRanges` | `string[]` | optional | Employer size, one or more of `1-10`, `11-50`, `51-200`, `201-500`, `501-1000`, `1001-5000`, `5001-10000`, `10001+`. |
| `locations` | `string[]` | optional | Metro areas, spelled the way a profile would: `"San Francisco Bay Area"`. |
| `countries` | `string[]` | optional | Country names, such as `"Germany"`. |
| `skills` | `string[]` | optional | Skills listed on the profile. |
| `minYearsExperience` | `number` | optional | Lower bound on total years of experience. |
| `maxYearsExperience` | `number` | optional | Upper bound on total years of experience. |
| `recentlyChangedJobs` | `boolean` | optional | Only people who started somewhere new recently. |
| `verifiedEmailOnly` | `boolean` | optional | Only people with a verified work email on file. |
| `currentEmployersOnly` | `boolean` | optional | Defaults to `true`. Set `false` to match the employer filters against past roles too, which is how you find people who used to be somewhere. |

An unknown value in an enum field comes back as a `400` naming the field, the
value, and the list it should have come from. Nothing is charged for a request
that never ran.

## Pagination

Pages are walked with an opaque cursor: pass back the `nextCursor` you were
given, change nothing else, and a `null` one means you have reached the end.

**TypeScript**

```typescript
async function everyone(query: object) {
  const all = [];
  let cursor: string | null = null;
  do {
    const res = await fetch("https://www.refolk.ai/api/v1/people/search", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.REFOLK_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ query, limit: 100, cursor }),
    });
    const page = await res.json();
    all.push(...page.profiles);
    cursor = page.nextCursor;
  } while (cursor);
  return all;
}
```

**Python**

```python
def everyone(query):
    all_rows, cursor = [], None
    while True:
        res = requests.post(
            "https://www.refolk.ai/api/v1/people/search",
            headers={"Authorization": f"Bearer {os.environ['REFOLK_API_KEY']}"},
            json={"query": query, "limit": 100, "cursor": cursor},
        )
        page = res.json()
        all_rows += page["profiles"]
        cursor = page["nextCursor"]
        if not cursor:
            return all_rows
```

Each page is 1 credit, so a walk over a large result
set costs one credit per 100 people. Check `totalCount` on the first
page before committing to the walk.

## Turning a sentence into a filter set

Write the brief once in English, keep the filter set it produces, and run the
structured search on a schedule from then on. One credit, and no search runs.

### `POST /api/v1/people/translate`

A sentence in, the filter set the structured search takes out. Nothing is searched.

**Cost:** 1 credit

**Body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `prompt` | `string` | required | Who you are looking for, in English. |
| `currentQuery` | `object` | optional | An existing filter set to amend rather than replace. |

**curl**

```bash
curl https://www.refolk.ai/api/v1/people/translate \
  -H "Authorization: Bearer $REFOLK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "senior platform engineers at mid-size fintechs in Berlin"}'
```

**Amending one**

```text
{
  "prompt": "same, but only 200 people and up",
  "currentQuery": {
    "titleIncludes": ["platform engineer"],
    "locations": ["Berlin"],
    "seniorityLevels": ["Senior"]
  }
}
```

Run this request in the playground: https://www.refolk.ai/developers/playground?endpoint=translate. One credit. No search runs.

```json
{"prompt": "senior platform engineers at mid-size fintechs in Berlin"}
```

The result is the filter set and nothing else, so you can show it to somebody,
store it, diff it against the last one, or edit a field by hand before you
spend anything on results.

## When to reach for the other one instead

Filters cannot express "shipped Rust in production", "maintains something
people depend on", or "was an engineer before founding". Those are judgements
about evidence, and they are what
[the plain English search](/developers/agentic-search) is for. Use filters when
you know the criteria and want them applied the same way every time.



---

## More of the API documentation

- [Quickstart](https://www.refolk.ai/developers/quickstart.md): Create a key, run your first search, and read what comes back. A working request in under two minutes, in curl, TypeScript, and Python.
- [Authentication](https://www.refolk.ai/developers/authentication.md): How API keys work: creating one, sending it, rotating it, and what happens when one leaks. Keys are shown once and hashed at rest.
- [Credits and limits](https://www.refolk.ai/developers/credits-and-limits.md): What each call costs, how a search is reserved and reconciled, what the rate limits are, and how to check a balance before a long run.
- [Playground](https://www.refolk.ai/developers/playground.md): Run any endpoint from the browser, signed in, on your own credits. Prefilled examples, the real response, and the equivalent curl for your own code.
- [Search in plain English](https://www.refolk.ai/developers/agentic-search.md): POST /v1/search. One sentence in, a ranked shortlist out, with the evidence behind every pick. Blocking or streamed as server-sent events.
- [Errors](https://www.refolk.ai/developers/errors.md): One error shape for every endpoint, a closed set of types, and which of them are worth retrying. What gets refunded when a call fails.
- [MCP server](https://www.refolk.ai/developers/mcp.md): Give Claude, Cursor, VS Code, or any MCP client the ability to search for people mid-conversation. One URL, one header, four tools.
- [OpenAPI document](https://www.refolk.ai/api/v1/openapi.json): the machine-readable specification.

Authenticate with `Authorization: Bearer rfk_live_...`. Keys are created at https://www.refolk.ai/hire/api.