Summary and recommendation
Personio exposes a REST API at https://api.personio.de/v1 using a proprietary Bearer token flow: POST credentials to /v1/auth and re-authenticate on 401. This is not RFC 6749 OAuth 2.0-standard OAuth client libraries expecting a /token endpoint will not work without modification.
API credentials are scoped at the attribute level in the Personio UI; fields not explicitly permitted are silently omitted from responses rather than returning an error. All field values in responses are wrapped in {"value": ..., "type": ...} objects and must be unwrapped before use.
Rate limiting is enforced at 200 requests per minute per credential; HTTP 429 is returned on breach, with X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers present but no Retry-After header documented.
API quick reference
| Has user API | Yes |
| Auth method | Bearer token (client_credentials grant via Personio's own token endpoint; not standard OAuth 2.0 per official docs) |
| Base URL | Official docs |
| SCIM available | No |
| SCIM plan required | Enterprise |
Authentication
Auth method: Bearer token (client_credentials grant via Personio's own token endpoint; not standard OAuth 2.0 per official docs)
Setup steps
- In Personio, navigate to Settings > Integrations > API Credentials.
- Create a new API credential set; assign the required attribute-level permissions (read/write per data category).
- Copy the generated Client ID and Client Secret.
- POST to https://api.personio.de/v1/auth with JSON body {"client_id": "
", "client_secret": " "} to receive a Bearer token. - Include the token in all subsequent requests as Authorization: Bearer
. - Tokens expire; re-authenticate when a 401 is returned.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| read:employees | Read employee profile attributes | GET /company/employees, GET /company/employees/{id} |
| write:employees | Create and update employee records | POST /company/employees, PATCH /company/employees/{id} |
| read:absences | Read absence/time-off records | GET /company/time-offs |
| write:absences | Create absence records | POST /company/time-offs |
| read:attendances | Read attendance records | GET /company/attendances |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | integer | Personio internal employee ID | system-generated | immutable | Used as path param for individual employee operations |
| first_name | string | Employee first name | required | optional | |
| last_name | string | Employee last name | required | optional | |
| string | Work email address | required | optional | Used for login; must be unique | |
| gender | string | Gender (male/female/diverse) | optional | optional | |
| status | string | Employment status (active/inactive/leave) | optional | optional | |
| position | string | Job title/position | optional | optional | |
| department | object | Department the employee belongs to | optional | optional | References department by name or ID |
| office | object | Office/location assignment | optional | optional | |
| hire_date | date (YYYY-MM-DD) | Date of hire | required | optional | |
| contract_end_date | date (YYYY-MM-DD) | End date for fixed-term contracts | optional | optional | Null for permanent contracts |
| weekly_working_hours | number | Contracted weekly hours | optional | optional | |
| supervisor | object | Direct manager reference | optional | optional | Contains supervisor employee ID |
| cost_centers | array | Cost center assignments with percentage splits | optional | optional | |
| team | object | Team assignment | optional | optional | |
| subcompany | object | Legal entity / subcompany | optional | optional | |
| employee_type | string | Employment type (internal/external) | optional | optional | |
| profile_picture | string (URL) | URL to employee profile image | optional | optional | Read-only via API |
| dynamic_fields | object | Custom attribute fields defined in Personio configuration | optional | optional | Field keys vary per company configuration |
Core endpoints
Authenticate / Get Token
- Method: POST
- URL:
https://api.personio.de/v1/auth - Watch out for: Token lifetime is not explicitly documented; treat as short-lived and re-authenticate on 401.
Request example
POST /v1/auth
Content-Type: application/json
{
"client_id": "your_client_id",
"client_secret": "your_client_secret"
}
Response example
{
"success": true,
"data": {
"token": "eyJ..."
}
}
List Employees
- Method: GET
- URL:
https://api.personio.de/v1/company/employees - Watch out for: Response wraps each field in {"value": ..., "type": ...} objects. Max 200 records per page; iterate with offset for full dataset.
Request example
GET /v1/company/employees?limit=200&offset=0
Authorization: Bearer <token>
Response example
{
"success": true,
"data": [
{"type": "Employee", "attributes": {"id": {"value": 123}, "email": {"value": "jane@example.com"}}}
]
}
Get Single Employee
- Method: GET
- URL:
https://api.personio.de/v1/company/employees/{id} - Watch out for: Only attributes enabled in the API credential's permission set are returned.
Request example
GET /v1/company/employees/123
Authorization: Bearer <token>
Response example
{
"success": true,
"data": {
"type": "Employee",
"attributes": {
"id": {"value": 123},
"email": {"value": "jane@example.com"}
}
}
}
Create Employee
- Method: POST
- URL:
https://api.personio.de/v1/company/employees - Watch out for: Creating an employee does NOT automatically send a Personio login invitation. A separate action in the UI or via the invite endpoint is needed.
Request example
POST /v1/company/employees
Authorization: Bearer <token>
Content-Type: application/json
{
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com",
"hire_date": "2024-01-15"
}
Response example
{
"success": true,
"data": {
"id": 456,
"message": "Employee created."
}
}
Update Employee
- Method: PATCH
- URL:
https://api.personio.de/v1/company/employees/{id} - Watch out for: Custom/dynamic attributes require the exact internal attribute key as configured in Personio; these keys differ per company.
Request example
PATCH /v1/company/employees/456
Authorization: Bearer <token>
Content-Type: application/json
{
"position": "Senior Engineer",
"department": "Engineering"
}
Response example
{
"success": true,
"data": {
"message": "Employee updated."
}
}
List Absences (Time-offs)
- Method: GET
- URL:
https://api.personio.de/v1/company/time-offs - Watch out for: Date range filters are required; unbounded queries may be rejected or return unexpected results.
Request example
GET /v1/company/time-offs?start_date=2024-01-01&end_date=2024-12-31
Authorization: Bearer <token>
Response example
{
"success": true,
"data": [
{"id": 1, "employee_id": 123, "type": "Vacation", "start_date": "2024-06-01"}
]
}
List Attendances
- Method: GET
- URL:
https://api.personio.de/v1/company/attendances - Watch out for: Attendance data access requires the attendance read permission on the API credential.
Request example
GET /v1/company/attendances?start_date=2024-01-01&end_date=2024-01-31
Authorization: Bearer <token>
Response example
{
"success": true,
"data": [
{"id": 99, "employee": {"id": 123}, "date": "2024-01-10", "start_time": "09:00", "end_time": "17:00"}
]
}
Get Employee Profile Picture
- Method: GET
- URL:
https://api.personio.de/v1/company/employees/{id}/profile-picture/{width} - Watch out for: Returns binary image data, not JSON. Width parameter is required (pixels).
Request example
GET /v1/company/employees/123/profile-picture/200
Authorization: Bearer <token>
Response example
Binary image data (JPEG/PNG) returned directly in response body.
Rate limits, pagination, and events
- Rate limits: Personio enforces rate limits per API credential. Official docs state a limit of 200 requests per minute per credential as of the current API version.
- Rate-limit headers: Yes
- Retry-After header: No
- Rate-limit notes: When the limit is exceeded, the API returns HTTP 429. Headers X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset are included in responses. Retry-After header is not documented as standard.
- Pagination method: offset
- Default page size: 200
- Max page size: 200
- Pagination pointer: limit / offset
| Plan | Limit | Concurrent |
|---|---|---|
| All plans (API access) | 200 requests/minute | 0 |
- Webhooks available: Yes
- Webhook notes: Personio supports webhooks for employee-related events. Webhooks are configured in Settings > Integrations > Webhooks. Payloads are sent as HTTP POST to a configured URL.
- Alternative event strategy: Poll GET /company/employees with updated_since filter parameter for change detection if webhooks are not feasible.
- Webhook events: employee.created, employee.updated, employee.deleted, absence.created, absence.updated, absence.deleted, attendance.created, attendance.updated, attendance.deleted
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: Enterprise
- Endpoint: Not documented
Limitations:
- Personio does not offer a native SCIM 2.0 endpoint.
- Automated provisioning is available via Microsoft Entra ID integration (uses Personio's REST API under the hood, not SCIM protocol).
- JumpCloud can provide a SAML bridge for SSO but does not enable SCIM provisioning natively.
- Okta integration is available for SSO (OIDC) but SCIM provisioning is not natively supported.
- Enterprise plan required for SSO and advanced integrations; provisioning automation requires custom integration or third-party middleware.
Common scenarios
Three integration patterns cover the majority of use cases. For onboarding, POST to /v1/company/employees with the required fields, capture the returned employee ID, then PATCH additional attributes in a follow-up call-note that employee creation does not trigger a Personio login invitation automatically.
For incremental sync into a data warehouse or identity graph, use GET /v1/company/employees?
updated_since=
For SSO provisioning without native SCIM, the Microsoft Entra ID connector (Enterprise plan only) maps Entra ID attributes to Personio fields and uses Personio's REST API under the hood-this is not SCIM 2. 0, and attribute mapping is constrained to what Personio's connector exposes.
Onboard a new employee and sync to downstream systems
- POST /v1/auth to obtain Bearer token.
- POST /v1/company/employees with first_name, last_name, email, hire_date, department, position.
- Capture the returned employee ID from the response.
- PATCH /v1/company/employees/{id} to set additional attributes (supervisor, cost_center, custom fields).
- Trigger downstream provisioning (e.g., Entra ID sync) using the Personio employee ID as the source-of-truth identifier.
- Manually or via UI send Personio login invitation if the employee needs Personio access.
Watch out for: Employee creation does not auto-invite the user to Personio. Custom attribute keys must be known in advance and are company-specific.
Incremental employee sync to a data warehouse
- POST /v1/auth to obtain token.
- GET /v1/company/employees?updated_since=
&limit=200&offset=0. - Iterate pages by incrementing offset until data array is empty.
- Unwrap the nested {value, type} field structure for each employee attribute.
- Upsert records into the target system using Personio employee ID as the primary key.
- Store the current timestamp as last_sync_timestamp for the next run.
Watch out for: Ensure timezone consistency for updated_since; Personio expects ISO 8601 format. Deleted employees are not returned; handle offboarding separately via webhooks or status checks.
SSO provisioning via Microsoft Entra ID (no native SCIM)
- Confirm Enterprise plan is active in Personio.
- In Personio Settings > Integrations, enable the Microsoft Entra ID (Azure AD) integration.
- In Entra ID, configure the Personio enterprise application using the OIDC credentials provided by Personio.
- Map Entra ID user attributes to Personio employee fields within the Entra ID provisioning configuration.
- Enable automatic provisioning in Entra ID; Personio's integration uses its REST API (not SCIM) under the hood.
- Test with a pilot user group before enabling for all users.
Watch out for: This is not native SCIM; it is a Personio-built Entra ID connector. Provisioning capabilities and attribute mappings are limited to what Personio's connector supports, not the full SCIM 2.0 attribute set.
Why building this yourself is a trap
Personio has no native SCIM 2.0 endpoint. Automated provisioning requires either the Entra ID connector (Enterprise plan, limited attribute coverage) or a purpose-built integration.
For teams building against the REST API to populate an identity graph, two structural issues compound each other: custom attribute keys are company-specific and must be discovered at runtime via a GET call rather than being predictable across tenants, and the nested {value, type} response envelope means field extraction requires an unwrap step that breaks naive JSON path assumptions.
Okta supports OIDC-based SSO with Personio but does not enable SCIM provisioning natively. JumpCloud can provide a SAML bridge for SSO but does not add SCIM provisioning either.
Any integration that assumes standard SCIM semantics will require a middleware layer or a translation component-an MCP server with 60+ deep IT/identity integrations is one approach to avoid building and maintaining that translation layer in-house.
Automate Personio workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.