---
updatedAt: 2026-06-17T06:13:04.000Z
---

Fetch the complete documentation index at: https://apidocs.hibob.com/llms.txt. Use this file to discover all available pages before exploring further.

# People read API contract

Request and response rules for POST /people/search and POST /people/{identifier} — field IDs, permissions, filters, no pagination (full result set), silent omissions, and payload shapes.

This page documents how `POST /people/search` and `POST /people/{identifier}` behave in practice. Use it when integrating or debugging read flows. For a step-by-step setup guide, see [Read employee data](/docs/how-to-read-employee-data).

## Endpoints covered

| Endpoint                                                                         | Purpose                                               |
| :------------------------------------------------------------------------------- | :---------------------------------------------------- |
| [Search for employees](/reference/post_people-search)                            | Read fields for many employees; optional filters      |
| [Read company employee fields by employee ID](/reference/post_people-identifier) | Read fields for one employee (by backend ID or email) |

Both endpoints accept `fields` and `humanReadable`. Search also accepts `filters` and `showInactive` (defaults to active employees only — see [showInactive](#showinactive-terminated-and-former-employees)). Response semantics below apply to both unless noted.

***

## Read-after-update consistency

Recently updated employee data may take a few seconds to become available when fetching data. If you update employee data and then immediately retrieve the same data, the response may still show the previous value or `null`.

To ensure the latest data is returned, add a short delay (up to **20 seconds**) before making a subsequent read request.

This applies when you read via Search Employee shortly after an update (for example, via Update Company Employee). For update workflows, see [Update employee data](/docs/update-employee-data#read-after-update-consistency).

***

## Pagination and result scope

These endpoints **do not support cursor-based pagination**. There are no `limit`, `cursor`, or `response_metadata.next_cursor` request or response fields. Each call returns a **single complete result set** in one HTTP response. This differs from paginated Employee data endpoints such as [Employee Tables bulk reads](/reference/get_bulk-people-work) and [Search for actual payments](/reference/post_people-actual-payments-search). See [Pagination](/reference/pagination-1).

| Endpoint                                                 | Bulk read?               | What one successful call returns                                                                       |
| :------------------------------------------------------- | :----------------------- | :----------------------------------------------------------------------------------------------------- |
| [Search for employees](/reference/post_people-search)    | **Yes** — many employees | **All** employees that match the request and the service user's access scope, in one `employees` array |
| [Read by employee ID](/reference/post_people-identifier) | **No** — one employee    | **One** employee object (identified by backend ID or email in the path)                                |

### `POST /people/search` — all matches, no paging

* **Without&#x20;**`filters`: returns every employee the service user can access (respecting `showInactive`). There is **no server-side page size** and **no truncation** of the employee list in the response.
* **With a&#x20;**`root.id`**&#x20;or&#x20;**`root.email`**&#x20;filter**: returns every employee whose ID or email appears in the filter `values` array (operator must be `equals`).
* **Request limits** (not pagination): at most **one** filter object; at most **400** field IDs in `fields[]`; invalid filter parameters return `400`.
* **Rate limiting** still applies per HTTP request (see [People API](/reference/people)); fetching a very large company in one call can produce a large payload and counts as one request toward the limit.

### Recommended pattern for large companies (client-side batching)

> **For integrators and AI agents:** When you need data for **all** employees in a large company, **do not** call search once with many fields and no filter (or with the default field set). That returns every employee with a heavy payload in a single response. **Recommended approach:**

1. **Discover IDs (lightweight call)** — Search with only identifiers, for example `fields: ["root.id"]` (and `root.email` only if you need it). Omit `filters` to list everyone the service user can access. Set `showInactive: true` when terminated or inactive employees must be included (default excludes them).
2. **Fetch details in batches** — Split the collected IDs into chunks (for example **50–200 IDs per request**, tuned to how many fields you request and your timeout budget). For each chunk, call search again with the full `fields[]` you need and one filter:

```json
{
  "fields": ["root.id", "root.firstName", "work.department"],
  "filters": [{
    "fieldPath": "root.id",
    "operator": "equals",
    "values": ["3332883884017713938", "3332883909410030364"]
  }]
}
```

This is **client-side pagination**: the API does not expose pages, but you control payload size and failure blast radius. The same pattern applies when an agent must sync an entire directory.

***

## showInactive (terminated and former employees)

`showInactive` is a request body flag on `POST /people/search`**&#x20;only**. It defaults to `false`.

| Value              | Who is returned                                                                                                    |
| :----------------- | :----------------------------------------------------------------------------------------------------------------- |
| Omitted or `false` | **Active** employees only (`internal.status` = Active)                                                             |
| `true`             | **Active and inactive** employees (inactive = typically **terminated** / **former**; `internal.status` = Inactive) |

**When to set&#x20;**`showInactive: true`

* The user or integration asks for **terminated**, **inactive**, **former**, **offboarded**, or **leaver** employees.
* You need a full directory that includes leavers (for example `fields: ["root.id"]` with inactive included).
* You read `internal.status` or `internal.lifecycleStatus` for the whole company, not only current employees.

**AI agents:** If the request mentions terminated, inactive, former, or leaver employees, set `"showInactive": true`. With the default, those employees are **excluded** even when you know their ID or email. If the user asks for “all employees”, “everyone”, or a “full directory” without saying whether leavers are included, **ask a clarifying question** before searching (active-only vs including terminated/former); do not assume.

**Permissions:** `showInactive: true` is not enough on its own. The service user must also be allowed to access inactive employee data in Bob (for example permission group **Access data for** without a “Lifecycle status equals Employed” restriction only). **Agents cannot grant permissions** — if results omit terminated employees despite the flag, tell the user a Bob admin must update the service user permission group. See [How to read the employee status](/docs/how-to-read-employee-data-tutorial#how-to-read-the-employee-status).

**Single-employee read:** [`POST /people/{identifier}`](/reference/post_people-identifier) does **not** use this flag in the request body; the backend resolves the employee by ID or email whether they are active or inactive, subject to service user access.

**Example — list terminated/inactive employees (IDs only):**

```json
{
  "showInactive": true,
  "fields": ["root.id", "internal.status", "internal.lifecycleStatus"]
}
```

**Avoid:**

* Assuming search returns terminated employees when `showInactive` is omitted (default excludes them).
* Setting `showInactive: true` without granting the service user access to inactive lifecycle data.

**Avoid (large companies):**

* `{}` or a wide default `fields` set with no `filters` when the company has thousands of employees and you need rich field data.
* Assuming a follow-up call will return fewer employees unless you pass a `root.id` filter.

**Note:** Bob does not document a maximum number of IDs per filter `values` array; very large `values` lists may still produce heavy responses. Prefer smaller batches when requesting many fields per employee.

### `POST /people/{identifier}` — single employee

This endpoint reads **one** employee per call. It is not a bulk search endpoint; use [Search for employees](/reference/post_people-search) when you need many employees in one request.

***

## Prerequisites checklist

Before calling either endpoint, complete these steps in order:

1. **Discover field IDs** — Call [GET /company/people/fields](/reference/get_company-people-fields). Each field's `id` (e.g. `root.id`, `work.department`) is the **canonical field ID** to use in requests.
2. **Find the category** — From the same metadata, read `categoryId` and `categoryDisplayName`. Permissions are granted by **category**, not by individual field ID. Do not infer category from the field ID string alone; fields can be moved between categories without changing their ID. See [Categories and permissions](/docs/categories-and-permissions).
3. **Grant service user permissions** — In Bob, give the service user **View** access to each required category under **People's data**, plus the correct **Access data for** scope (active, terminated, etc.). See [API Service Users](/docs/api-service-users) and [Categories and permissions](/docs/categories-and-permissions).
4. **Build the request** — Pass field IDs from metadata in the `fields` array using **dot notation** (see [Field ID notation](#field-id-notation)).
5. **Validate the response** — After a `200 OK`, compare requested fields against the response. Missing fields usually mean permissions or an invalid field ID, not an HTTP error (see [Silent omission](#silent-omission)).

***

## Field ID notation

### Requests — use dot notation (canonical)

Field IDs returned by [Fields metadata](/reference/get_company-people-fields) use **dot notation**:

```json
"id": "work.department"
```

Always use this form in `fields[]` and in filter `fieldPath`:

```json
{
  "fields": ["root.id", "root.firstName", "work.department"],
  "filters": [{
    "fieldPath": "root.id",
    "operator": "equals",
    "values": ["3332883884017713938"]
  }]
}
```

This matches metadata and is the **canonical field ID**. Copy values directly from the metadata `id` property.

> **Field IDs are stable — not category paths**
>
> A field ID such as `work.department` can look like `category.field`, but it is **not** a path to the field's current category in Bob. It is a **stable identifier** assigned when the field was created. If an admin moves the field to another category (e.g. from Work to a custom category), the field ID **stays the same** — only `categoryId` in metadata changes.
>
> **Do not** infer permissions from the field ID prefix. Always read `categoryId` from [Fields metadata](/reference/get_company-people-fields) and grant service user access to that category. See [Moving fields between categories](/docs/categories-and-permissions#moving-fields-between-categories).

> **Note:** Slash notation (e.g. `/root/id`) may be accepted in some requests, but dot notation is canonical. Prefer dot notation everywhere in requests.

### Responses — slash notation

Response payloads use **slash notation** for machine-format field keys:

```json
"/root/id": { "value": "3332883884017713938" },
"/work/department": { "value": "209192163" }
```

When parsing responses, look for keys in `/category/field` form. Map back to metadata using dot notation by replacing `/` with `.` (e.g. `/root/id` → `root.id`).

### Dual shapes in one employee object

A single employee in the response may contain **both**:

* Slash keys: `"/work/siteId": { "value": 2510068 }`
* Nested category objects: `"work": { "siteId": 2510068, "department": "Product" }`

These represent the same underlying data in different JSON layouts. When building parsers:

* Prefer `/category/field`**&#x20;keys** when present (consistent machine-format `{ "value": ... }` wrapper).
* Treat nested category objects as an additional convenience shape; do not assume every requested field appears in both forms.

See [Fields metadata — Field ID notation](/docs/fields-metadata#field-id-notation) for `humanReadable` effects on response shape.

***

## Silent omission

The API returns `200 OK` even when some requested fields are missing. There is **no warning** in the response body or headers.

| Situation                                                          | HTTP status | Result                                                                                     |
| :----------------------------------------------------------------- | :---------- | :----------------------------------------------------------------------------------------- |
| Field in `fields[]` — service user lacks category permission       | 200         | Field **absent**                                                                           |
| Field in `fields[]` — unknown or invalid field ID                  | 200         | Field **absent**                                                                           |
| Field in `fields[]` — `humanReadable: "REPLACE"` and value is null | 200         | Field **absent**                                                                           |
| Empty or omitted `fields[]`                                        | 200         | **Default field set** (categories: root, about, employment, work — subject to permissions) |
| Filter `fieldPath` not `root.id` or `root.email`                   | **400**     | Error (filters fail loudly)                                                                |
| Filter operator not `equals`                                       | **400**     | Error                                                                                      |
| Empty filter `values`                                              | **400**     | Error                                                                                      |

**Important:** Silent omission applies to `fields[]`, not to `filters`. Invalid filter parameters return `400`.

### Integration checklist after `200 OK`

1. List every field ID you sent in `fields[]`.
2. For each, check whether a corresponding `/category/field` key (or expected nested value) exists in the employee object.
3. If missing: verify the field ID in metadata → verify `categoryId` → verify service user **View** permission on that category → verify **Access data for** includes that employee (e.g. terminated lifecycle).

***

## Filters (search only)

`POST /people/search` supports **one filter object** per request.

| Property    | Allowed values                                             |
| :---------- | :--------------------------------------------------------- |
| `fieldPath` | `root.id`, `root.email` only                               |
| `operator`  | `equals` only                                              |
| `values`    | Non-empty array of employee backend IDs or email addresses |

You **cannot** filter employees by other fields (e.g. `work.department`, `work.siteId`). To narrow results by other criteria, fetch the fields you need and filter client-side, or filter by `root.id` / `root.email` when you already know those values.

If `filters` is omitted, the response includes **all** employees the service user can access (respecting `showInactive`) in a single response — see [Pagination and result scope](#pagination-and-result-scope).

***

## Permissions model

* Permissions are granted at the **category** level (e.g. `root`, `work`, `category_1720508362305`).
* If the service user can view a category, all fields in that category can be returned (when requested or included in the default set).
* If the service user **cannot** view a category, fields in that category are **silently omitted** — even if listed in `fields[]`.
* Default search (no `fields[]`) returns fields from **root**, **about**, **employment**, and **work** only. Fields moved to other categories require explicit `fields[]` and matching permissions. See [Moving fields between categories](/docs/categories-and-permissions#moving-fields-between-categories).

***

## Default fields

When `fields` is omitted or empty, both read endpoints return a predefined set covering basic info and the **about**, **employment**, and **work** categories — limited by service user permissions. To read fields outside that set, pass explicit field IDs from metadata.

Table data (work history rows, salaries, etc.) is **not** returned by these endpoints. Use [Employee Tables](/reference/employee-tables). Custom tables require [Custom Tables](/reference/custom-tables) endpoints.

***

## Related guides

* [Read employee data](/docs/how-to-read-employee-data) — step-by-step integration walkthrough
* [Update employee data](/docs/update-employee-data) — updating fields and [read-after-update consistency](/docs/update-employee-data#read-after-update-consistency)
* [Fields metadata](/docs/fields-metadata) — field types, lists, `humanReadable`, custom data shapes
* [Categories and permissions](/docs/categories-and-permissions) — mapping metadata categories to Bob permissions
* [People API reference](/reference/people) — rate limits, permissions table, troubleshooting

<br />