Developers
Employees API
Reference for managing employee records via the Performance Blocks API — CRUD, status transitions, CSV upsert, and reporting lines.
Plan availability: The Performance Blocks API is available on the Agentic plan. Team plan customers can upgrade in Settings → Billing.
The Employees API is the system of record for the people in your Performance Blocks organization. It supports full CRUD, lifecycle transitions (deactivate / reactivate / rehire), reporting line management, custom fields, and a bulk CSV upsert designed for HRIS sync workflows.
This is the API you wire up first when building any integration. Other resources reference employees by ID; an HRIS-synced employee directory is the foundation for observations, summaries, and objectives that line up with your real org chart.
Resource shape
{
"id": "emp_01HXX9YK7Z8N2P3Q4R5S6T7U8V",
"email": "jordan.reyes@example.com",
"first_name": "Jordan",
"last_name": "Reyes",
"role": "manager",
"manager_id": "emp_01HYY...",
"department_id": "dep_01HZZ...",
"status": "active",
"custom_fields": {
"employee_number": "E-04321",
"location": "remote-us",
"start_date": "2024-08-12"
},
"created_at": "2024-08-12T14:00:00.000Z",
"updated_at": "2026-04-01T09:30:00.000Z",
"deactivated_at": null
}
Field reference
| Field | Type | Notes |
|---|---|---|
id |
string | ULID prefixed with emp_. Read-only. |
email |
string | Unique within the organization. Required. |
first_name |
string | 1–80 characters. Required. |
last_name |
string | 1–80 characters. Required. |
role |
enum | employee, manager, org_admin. Defaults to employee. |
manager_id |
string | null | The employee's manager. null for the top of a reporting tree. |
department_id |
string | null | Optional department reference. |
status |
enum | active, deactivated, pending_invite. Read-only — change via lifecycle endpoints. |
custom_fields |
object | Arbitrary key/value map. Strings, numbers, booleans, and ISO dates only — no nested objects. Up to 32 keys, key length ≤ 64 chars, value length ≤ 1024 chars. |
created_at |
string (datetime) | Read-only. |
updated_at |
string (datetime) | Read-only. |
deactivated_at |
string (datetime) | null | Set when status is deactivated. |
HRIS-managed fields
If an employee is sourced from an HRIS connector, certain fields are managed externally and become read-only in this API:
emailfirst_namelast_namemanager_iddepartment_idcustom_fieldskeys mapped from HRIS attributes
Attempts to write these fields on an HRIS-managed employee return 422 unprocessable with a field_managed_externally detail. The role field, status transitions, and custom_fields keys not mapped from HRIS remain writable.
The current is_hris_managed flag and hris_source (e.g. workday, bamboohr) are returned on every employee resource as read-only metadata.
Expand paths
?expand= accepts:
manager— inlines the manager Employee object.department— inlines the Department object.direct_reports— inlines an array of direct-report Employee objects (limited to 100; use/employees?filter[manager_id]=...for full paging).
List employees
GET /employees
Query parameters
| Parameter | Type | Notes |
|---|---|---|
limit |
integer | Default 50, max 100. |
cursor |
string | Pagination. |
sort |
string | last_name, first_name, created_at, -created_at. Default last_name. |
filter[email] |
string | Exact match. |
filter[role] |
enum | Repeat for multiple. |
filter[manager_id] |
string | Direct reports of this manager. |
filter[department_id] |
string | Members of this department. |
filter[status] |
enum | active (default), deactivated, pending_invite, all. |
filter[custom_fields.<key>] |
string | Exact match on a custom field value. |
expand |
string | See above. |
Example: curl
curl "https://api.performanceblocks.com/v1/employees\
?filter[manager_id]=emp_01HYY...\
&filter[status]=active\
&limit=100" \
-H "Authorization: Bearer $PB_API_KEY"
Get an employee
GET /employees/{id}
curl https://api.performanceblocks.com/v1/employees/emp_01HXX... \
-H "Authorization: Bearer $PB_API_KEY"
Returns 404 if the employee is not in your organization.
Create an employee
POST /employees
Required scope: employees:write.
Request body
{
"email": "casey.lin@example.com",
"first_name": "Casey",
"last_name": "Lin",
"role": "employee",
"manager_id": "emp_01HYY...",
"department_id": "dep_01HZZ...",
"custom_fields": {
"employee_number": "E-04999",
"location": "remote-eu",
"start_date": "2026-05-15"
},
"send_invite": true
}
| Field | Notes |
|---|---|
email |
Required. Must be a valid email and unique in your organization (across all statuses). |
first_name, last_name |
Required. |
role |
Optional. Defaults to employee. |
manager_id |
Optional. Must reference an active employee in your organization. |
department_id |
Optional. Must reference a department in your organization. |
custom_fields |
Optional. See size limits in field reference. |
send_invite |
Optional, default false. If true, the new employee receives an invitation email and starts in pending_invite status until they accept. If false, the employee is created in active status with no invite. |
A successful create returns 201 with the new resource.
Example: TypeScript
const res = await fetch('https://api.performanceblocks.com/v1/employees', {
method: 'POST',
headers: {
Authorization: `Bearer
Update an employee
PATCH /employees/{id}
Required scope: employees:write.
PATCH is partial. Send only the fields you're changing. To clear manager_id or department_id, send null.
curl -X PATCH https://api.performanceblocks.com/v1/employees/emp_01HXX... \
-H "Authorization: Bearer $PB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"manager_id": "emp_01HZZ...", "role": "manager"}'
Restrictions
- HRIS-managed fields cannot be updated via the API.
- Setting
manager_idto a value that would create a reporting cycle returns422. - Setting
manager_idon a deactivated employee is allowed (for backfill), but the employee remains deactivated.
Reporting line management
Reporting lines are modeled as the manager_id foreign key. To restructure:
- Update the employee's
manager_iddirectly. Their direct reports (if any) are not affected. - To bulk re-parent direct reports under a new manager, page through
GET /employees?filter[manager_id]={old}andPATCHeach one with the newmanager_id.
There is no atomic "move team" endpoint. For large reorgs, consider running the moves in a single window and using the CSV upsert below.
To find an employee's full reporting chain, expand manager and walk up. To find their full subtree, page GET /employees?filter[manager_id]=... recursively (or fetch once with ?expand=direct_reports for the immediate level).
Status transitions
Deactivate
POST /employees/{id}/deactivate
Sets status to deactivated and deactivated_at to now. The employee can no longer log in. Their historical observations, summaries, and objectives are preserved. Returns 409 if already deactivated.
curl -X POST https://api.performanceblocks.com/v1/employees/emp_01HXX.../deactivate \
-H "Authorization: Bearer $PB_API_KEY"
Reactivate
POST /employees/{id}/reactivate
Reverses a recent deactivation. Sets status to active and clears deactivated_at. The employee's old reporting line and custom fields are preserved as-is. Returns 409 if not currently deactivated.
Rehire
POST /employees/{id}/rehire
For employees who left and returned. Differs from reactivate in that it:
- Sets
statustoactiveand clearsdeactivated_at. - Optionally accepts a body to update fields on rehire (new
manager_id, newdepartment_id, updatedcustom_fields). - Logs a separate
employee.rehiredevent for HR analytics.
{
"manager_id": "emp_01HZZ...",
"department_id": "dep_01HXX...",
"custom_fields": { "rehire_date": "2026-05-01" }
}
Returns the updated resource.
CSV bulk upsert
For HRIS sync, use the CSV endpoint instead of one-by-one POSTs. It dedupes by email and runs idempotently.
POST /employees/csv
Content-Type: text/csv
Required scope: employees:write.
The request body is a CSV file with a header row. Recognized columns:
| Column | Type | Notes |
|---|---|---|
email |
string | Required. Used as the upsert key. |
first_name |
string | Required for new employees. |
last_name |
string | Required for new employees. |
role |
enum | Optional. |
manager_email |
string | Optional. Resolved to manager_id. Both rows can be in the same file; the API resolves dependencies. |
department_name |
string | Optional. Resolved to department_id. Department is created if missing (org admin only). |
status |
enum | Optional. active or deactivated. Use deactivated to terminate. |
custom.<key> |
string | Custom field columns. Prefix with custom. (e.g. custom.employee_number). |
Example
email,first_name,last_name,manager_email,department_name,custom.employee_number
ceo@example.com,Pat,Smith,,,E-00001
vp@example.com,Sam,Patel,ceo@example.com,Engineering,E-00010
ic@example.com,Jordan,Reyes,vp@example.com,Engineering,E-00100
Request
curl -X POST https://api.performanceblocks.com/v1/employees/csv \
-H "Authorization: Bearer $PB_API_KEY" \
-H "Content-Type: text/csv" \
--data-binary @employees.csv
Response
{
"data": {
"created": 12,
"updated": 47,
"deactivated": 3,
"skipped": 0,
"errors": []
}
}
If any row fails validation, that row is reported in errors (with row_number, email, code, message) and the rest of the file is still processed. The upsert is best-effort batch, not transactional — partial success is the expected outcome.
Maximum file size 5 MB or 10,000 rows, whichever is smaller. For larger imports, split the file.
Webhook events
| Event | Fires when |
|---|---|
employee.created |
Employee is created via API or CSV. |
employee.updated |
Any field changes (including HRIS-driven changes). |
employee.deactivated |
Status moves to deactivated. |
employee.reactivated |
Status moves from deactivated to active via reactivate. |
employee.rehired |
Rehire endpoint is called. |
The delivery data field contains the full Employee resource. See Webhooks.
Common error responses
| Status | Code | Cause |
|---|---|---|
| 400 | validation_error |
Missing required field, invalid email, custom field too large. |
| 401 | authentication_required |
Missing or invalid API key. |
| 403 | permission_denied |
Key lacks employees:write. |
| 404 | not_found |
Employee not in your organization. |
| 409 | conflict |
Email already exists; status transition not allowed; reporting cycle. |
| 422 | unprocessable |
HRIS-managed field write attempt; manager not in your organization. |
Use cases
One-way HRIS sync
Run a daily job that exports your HRIS to CSV and POSTs to /employees/csv. Mark status=deactivated for terminations. The endpoint handles ordering (managers before reports) automatically. Track errors from each run and alert on non-zero.
Reactive sync via webhooks
If your HRIS supports outbound webhooks, translate each event to a single POST /employees or PATCH /employees/{id} request. Use a stable idempotency key derived from the HRIS event ID. Retry with backoff on 429 and 5xx.
Onboarding automation
When a new hire's start date arrives, your onboarding system calls POST /employees with send_invite: true. The employee receives the invitation, accepts, and starts in active status. Subscribe to employee.created to trigger downstream actions (provision Slack, schedule first 1:1, etc.).
Related
- API overview — pagination, expand, idempotency.
- Authentication and API keys
- Webhooks — real-time employee events.