People read API contract
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.
Endpoints covered
| Endpoint | Purpose |
|---|---|
| Search for employees | Read fields for many employees; optional filters |
| Read company employee fields by employee ID | 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). Response semantics below apply to both unless noted.
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 and Search for actual payments. See Pagination.
| Endpoint | Bulk read? | What one successful call returns |
|---|---|---|
| Search for employees | Yes — many employees | All employees that match the request and the service user's access scope, in one employees array |
| Read by employee ID | No — one employee | One employee object (identified by backend ID or email in the path) |
POST /people/search — all matches, no paging
POST /people/search — all matches, no paging- Without
filters: returns every employee the service user can access (respectingshowInactive). There is no server-side page size and no truncation of the employee list in the response. - With a
root.idorroot.emailfilter: returns every employee whose ID or email appears in the filtervaluesarray (operator must beequals). - Request limits (not pagination): at most one filter object; at most 400 field IDs in
fields[]; invalid filter parameters return400. - Rate limiting still applies per HTTP request (see People API); 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:
- Discover IDs (lightweight call) — Search with only identifiers, for example
fields: ["root.id"](androot.emailonly if you need it). Omitfiltersto list everyone the service user can access. SetshowInactive: truewhen terminated or inactive employees must be included (default excludes them). - 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:
{
"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 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 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.statusorinternal.lifecycleStatusfor the whole company, not only current employees.
AI agents and MCP: If the request mentions terminated, inactive, former, or leaver employees, set "showInactive": true (public MCP tool parameter: includeTerminated: 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.
Single-employee read: 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):
{
"showInactive": true,
"fields": ["root.id", "internal.status", "internal.lifecycleStatus"]
}Avoid:
- Assuming search returns terminated employees when
showInactiveis omitted (default excludes them). - Setting
showInactive: truewithout granting the service user access to inactive lifecycle data.
Avoid (large companies):
{}or a wide defaultfieldsset with nofilterswhen 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.idfilter.
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
POST /people/{identifier} — single employeeThis endpoint reads one employee per call. It is not a bulk search endpoint; use Search for employees when you need many employees in one request.
Prerequisites checklist
Before calling either endpoint, complete these steps in order:
- Discover field IDs — Call GET /company/people/fields. Each field's
id(e.g.root.id,work.department) is the canonical field ID to use in requests. - Find the category — From the same metadata, read
categoryIdandcategoryDisplayName. 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. - 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 and Categories and permissions.
- Build the request — Pass field IDs from metadata in the
fieldsarray using dot notation (see Field ID notation). - 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).
Field ID notation
Requests — use dot notation (canonical)
Field IDs returned by Fields metadata use dot notation:
"id": "work.department"Always use this form in fields[] and in filter fieldPath:
{
"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.departmentcan look likecategory.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 — onlycategoryIdin metadata changes.Do not infer permissions from the field ID prefix. Always read
categoryIdfrom Fields metadata and grant service user access to that category. See 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:
"/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/fieldkeys 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 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
200 OK- List every field ID you sent in
fields[]. - For each, check whether a corresponding
/category/fieldkey (or expected nested value) exists in the employee object. - 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.
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 explicitfields[]and matching permissions. See 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. Custom tables require Custom Tables endpoints.
Related guides
- Read employee data — step-by-step integration walkthrough
- Fields metadata — field types, lists,
humanReadable, custom data shapes - Categories and permissions — mapping metadata categories to Bob permissions
- People API reference — rate limits, permissions table, troubleshooting
Updated 2 days ago
