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

EndpointPurpose
Search for employeesRead fields for many employees; optional filters
Read company employee fields by employee IDRead 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.

EndpointBulk read?What one successful call returns
Search for employeesYes — many employeesAll employees that match the request and the service user's access scope, in one employees array
Read by employee IDNo — one employeeOne employee object (identified by backend ID or email in the path)

POST /people/search — all matches, no paging

  • Without 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 root.id or root.email 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); 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:
{
  "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.

ValueWho is returned
Omitted or falseActive employees only (internal.status = Active)
trueActive 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.status or internal.lifecycleStatus for 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 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 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. 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.
  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 and Categories and permissions.
  4. Build the request — Pass field IDs from metadata in the fields array using dot notation (see 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).

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.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 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/idroot.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 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 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.

SituationHTTP statusResult
Field in fields[] — service user lacks category permission200Field absent
Field in fields[] — unknown or invalid field ID200Field absent
Field in fields[]humanReadable: "REPLACE" and value is null200Field absent
Empty or omitted fields[]200Default field set (categories: root, about, employment, work — subject to permissions)
Filter fieldPath not root.id or root.email400Error (filters fail loudly)
Filter operator not equals400Error
Empty filter values400Error

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.

PropertyAllowed values
fieldPathroot.id, root.email only
operatorequals only
valuesNon-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 explicit fields[] 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