Summary and recommendation
The SAP SuccessFactors OData v2 API exposes user lifecycle operations under `/odata/v2/User` and, for Employee Central tenants, a separate entity model spanning `PerPerson`, `EmpEmployment`, and `EmpJob`.
Authentication requires OAuth 2.0 via SAML Bearer Assertion or X.509 mutual TLS-Basic Auth is deprecated and will be removed in November 2026.
The API user must hold explicit Role-Based Permission grants in Admin Center;
an OAuth token alone is insufficient without corresponding RBP assignments.
SCIM 2.0 is available via SAP Cloud Identity Services (Identity Provisioning Service) and is the recommended path for building an identity graph across enterprise systems, but it requires separate SAP Cloud Identity Services licensing, SSO as a prerequisite, and IPS transformation configuration for non-standard attribute mappings.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 (SAML Bearer Assertion) or X.509 certificate; Basic Auth deprecated November 2026 |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Requires SAP Cloud Identity Services (Identity Provisioning Service); available with Enterprise-tier SuccessFactors contracts. SSO prerequisite applies. |
Authentication
Auth method: OAuth 2.0 (SAML Bearer Assertion) or X.509 certificate; Basic Auth deprecated November 2026
Setup steps
- Register an OAuth 2.0 client application in SuccessFactors Admin Center under 'Manage OAuth2 Client Applications'.
- Generate or upload an X.509 certificate (or use SAML assertion) for the client application.
- Obtain a SAML assertion from your IdP signed with the registered certificate.
- POST the SAML assertion to https://
/oauth/token with grant_type=urn:ietf:params:oauth:grant-type:saml2-bearer to receive a Bearer access token. - Include the Bearer token in the Authorization header for all subsequent API requests.
- For service-to-service (no user context), use the OAuth 2.0 client credentials flow with X.509 mutual TLS as Basic Auth is being retired.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| API permission roles (not OAuth scopes) | SuccessFactors uses role-based permissions (RBP) rather than discrete OAuth scope strings. The API user must be granted specific Permission Roles in Admin Center. | All API operations |
| Manage Users | RBP permission allowing create, read, update, and deactivate of User records via OData API. | User CRUD via /User entity |
| View Users | RBP permission for read-only access to User and PerPerson entities. | GET /User, GET /PerPerson |
| Manage Employee Central | Required to read/write Employee Central entities such as EmpEmployment, EmpJob, and PerPersonal. | Employee Central compound employee operations |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| userId | String | Unique identifier for the user; maps to login name. | required | immutable | Primary key for the User entity. |
| username | String | Login username, often same as userId. | required | optional | |
| firstName | String | User's first name. | required | optional | |
| lastName | String | User's last name. | required | optional | |
| String | Primary email address. | required | optional | ||
| status | String | Account status: 'active', 'inactive', 'suspended'. | optional (defaults to active) | optional | Set to 'inactive' to deactivate; hard delete is generally not supported. |
| defaultLocale | String | User's preferred locale (e.g., en_US). | optional | optional | |
| timeZone | String | User's time zone (e.g., US/Eastern). | optional | optional | |
| department | String | Department name. | optional | optional | |
| division | String | Division name. | optional | optional | |
| title | String | Job title. | optional | optional | |
| manager | String (userId) | userId of the user's direct manager. | optional | optional | Navigation property; must reference a valid userId. |
| hr | String (userId) | userId of the assigned HR representative. | optional | optional | |
| hireDate | DateTime | Employee hire date. | optional | optional | Format: /Date(milliseconds)/ |
| lastModified | DateTime | Timestamp of last modification. | system-set | system-set | Read-only; useful for delta sync queries. |
| password | String | User password (write-only). | optional | optional | Never returned in GET responses. |
| empId | String | Employee ID, distinct from userId. | optional | optional | Used in Employee Central; links to PerPerson entity. |
| custom01–custom15 | String | Configurable custom fields on the User entity. | optional | optional | Field labels and usage are tenant-configured. |
Core endpoints
List Users
- Method: GET
- URL:
https://<host>/odata/v2/User?$top=100&$skip=0&$select=userId,firstName,lastName,email,status - Watch out for: Default page size is 20; always specify $top up to 1000. Use $filter=lastModified gt datetime'...' for delta syncs.
Request example
GET /odata/v2/User?$top=100&$skip=0&$select=userId,firstName,lastName,email,status
Authorization: Bearer <token>
Accept: application/json
Response example
{
"d": {
"results": [
{"userId":"jdoe","firstName":"Jane","lastName":"Doe","email":"jdoe@example.com","status":"active"}
]
}
}
Get Single User
- Method: GET
- URL:
https://<host>/odata/v2/User('<userId>') - Watch out for: userId is case-sensitive. Requesting a non-existent user returns HTTP 404.
Request example
GET /odata/v2/User('jdoe')?$select=userId,firstName,lastName,email,status
Authorization: Bearer <token>
Accept: application/json
Response example
{
"d": {
"userId":"jdoe",
"firstName":"Jane",
"lastName":"Doe",
"email":"jdoe@example.com",
"status":"active"
}
}
Create User
- Method: POST
- URL:
https://<host>/odata/v2/User - Watch out for: userId must be unique across the tenant. Employee Central hires require additional EmpEmployment and EmpJob entities created separately.
Request example
POST /odata/v2/User
Authorization: Bearer <token>
Content-Type: application/json
{"userId":"jdoe","firstName":"Jane","lastName":"Doe","email":"jdoe@example.com","status":"active"}
Response example
{
"d": {
"userId":"jdoe",
"firstName":"Jane",
"lastName":"Doe",
"email":"jdoe@example.com",
"status":"active"
}
}
Update User (full/partial)
- Method: PATCH
- URL:
https://<host>/odata/v2/User('<userId>') - Watch out for: OData v2 uses MERGE semantics for partial updates. Some clients must send X-HTTP-Method: MERGE header with a POST if PATCH is not supported.
Request example
PATCH /odata/v2/User('jdoe')
Authorization: Bearer <token>
Content-Type: application/json
X-HTTP-Method: MERGE
{"email":"jane.doe@example.com","title":"Senior Engineer"}
Response example
HTTP 204 No Content
Deactivate User
- Method: PATCH
- URL:
https://<host>/odata/v2/User('<userId>') - Watch out for: Hard delete of users is not supported via the standard OData API. Set status to 'inactive' to deactivate. Purge requires Admin Center or a separate data retention process.
Request example
PATCH /odata/v2/User('jdoe')
Authorization: Bearer <token>
Content-Type: application/json
X-HTTP-Method: MERGE
{"status":"inactive"}
Response example
HTTP 204 No Content
Batch Request
- Method: POST
- URL:
https://<host>/odata/v2/$batch - Watch out for: Batch requests reduce API call overhead. Maximum operations per batch are not publicly documented; SAP recommends keeping batches small to avoid timeouts.
Request example
POST /odata/v2/$batch
Authorization: Bearer <token>
Content-Type: multipart/mixed; boundary=batch_1
--batch_1
Content-Type: application/http
GET User('jdoe') HTTP/1.1
--batch_1--
Response example
HTTP 202 with multipart/mixed body containing individual operation responses.
Query Users (delta/filter)
- Method: GET
- URL:
https://<host>/odata/v2/User?$filter=lastModified gt datetime'2024-01-01T00:00:00'&$top=1000 - Watch out for: datetime filter format must match OData v2 syntax exactly. Timestamps are returned as /Date(ms)/ epoch format.
Request example
GET /odata/v2/User?$filter=lastModified%20gt%20datetime'2024-01-01T00:00:00'&$top=1000
Authorization: Bearer <token>
Accept: application/json
Response example
{
"d": {
"results": [
{"userId":"jdoe","lastModified":"/Date(1704067200000)/","status":"active"}
]
}
}
Get Compound Employee (Employee Central)
- Method: GET
- URL:
https://<host>/odata/v2/PerPerson('<personIdExternal>')?$expand=employmentNav,personalInfoNav - Watch out for: Employee Central uses a separate entity model (PerPerson, EmpEmployment, EmpJob) from the basic User entity. Both must be managed for full employee lifecycle in EC-enabled tenants.
Request example
GET /odata/v2/PerPerson('EMP001')?$expand=employmentNav,personalInfoNav
Authorization: Bearer <token>
Accept: application/json
Response example
{
"d": {
"personIdExternal":"EMP001",
"employmentNav":{"results":[{"userId":"jdoe","startDate":"/Date(1609459200000)/"}]},
"personalInfoNav":{"results":[{"firstName":"Jane","lastName":"Doe"}]}
}
}
Rate limits, pagination, and events
- Rate limits: SAP SuccessFactors enforces API concurrency and throughput limits at the tenant level. Specific numeric limits are not publicly documented and vary by data center and contract. SAP recommends using batch requests ($batch) to reduce call volume.
- Rate-limit headers: No
- Retry-After header: No
- Rate-limit notes: HTTP 429 or 503 responses may be returned when limits are exceeded. SAP documentation does not specify standard rate-limit response headers. Use exponential backoff and $batch to mitigate throttling.
- Pagination method: offset
- Default page size: 20
- Max page size: 1000
- Pagination pointer: $top / $skip
| Plan | Limit | Concurrent |
|---|---|---|
| All tenants | Not publicly specified; enforced per tenant by SAP operations | 0 |
- Webhooks available: Yes
- Webhook notes: SAP SuccessFactors supports intelligent services (event-based notifications) via the Intelligent Services Center. Events can trigger outbound HTTP calls or integration flows via SAP Integration Suite. This is not a traditional webhook registration API but an event-driven integration framework.
- Alternative event strategy: For near-real-time sync without Intelligent Services, use OData $filter on lastModified with scheduled polling. SAP Integration Suite (CPI) is the recommended middleware for event-driven integrations.
- Webhook events: New Hire, Termination, Job Information Change, Personal Information Change, Position Change, Leave of Absence
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Requires SAP Cloud Identity Services (Identity Provisioning Service); available with Enterprise-tier SuccessFactors contracts. SSO prerequisite applies.
Endpoint: Provisioned dynamically by SAP Identity Provisioning Service (IPS); format: https://
.accounts.ondemand.com/ips/service/scim/Users Supported operations: GET /Users, GET /Users/{id}, POST /Users, PUT /Users/{id}, PATCH /Users/{id}, DELETE /Users/{id}, GET /Groups, POST /Groups, PATCH /Groups/{id}, DELETE /Groups/{id}
Limitations:
- SCIM endpoint is managed by SAP IPS, not directly by SuccessFactors; endpoint URL is tenant-specific and not a fixed public URL.
- Basic Authentication for IPS is deprecated as of November 2026; must migrate to X.509 certificate-based authentication.
- Not all SuccessFactors custom fields are automatically mapped in SCIM; custom attribute mappings must be configured in IPS transformation rules.
- Employee Central compound employee data (EmpJob, EmpEmployment) requires IPS transformation configuration beyond standard SCIM User schema.
- SCIM DELETE maps to user deactivation (status=inactive), not hard delete.
- SSO (SAML) must be configured as a prerequisite for IPS-based SCIM provisioning.
Common scenarios
Three integration patterns cover the majority of lifecycle use cases.
For new hire provisioning, POST to /odata/v2/User for the base record, then create EmpEmployment and EmpJob entities separately if the tenant runs Employee Central-attempting EC entity creation on a non-EC tenant returns an error, so tenant type must be confirmed before integration design.
For delta sync, filter on lastModified gt datetime'<timestamp>' with $top=1000 and paginate via $skip;
note that EC entity changes (EmpJob, EmpEmployment) carry their own lastModifiedDateTime fields and must be queried independently to build a complete identity graph.
For offboarding, PATCH status to inactive using MERGE semantics (send X-HTTP-Method: MERGE if the client does not support PATCH natively);
hard delete is not available via API, and setting status=inactive does not terminate active SSO sessions-IdP-side session revocation must be coordinated separately.
Provision a new hire from an external HR system
- Authenticate: POST SAML assertion to /oauth/token to obtain Bearer token.
- Check for existing user: GET /odata/v2/User('
') – handle 404 as new user. - Create basic user record: POST /odata/v2/User with required fields (userId, firstName, lastName, email, status=active).
- If Employee Central is enabled, create PerPerson, EmpEmployment, and EmpJob entities via separate POST calls with the same personIdExternal.
- Assign manager: PATCH /odata/v2/User('
') with manager field set to manager's userId. - Verify creation: GET /odata/v2/User('
') and confirm status=active.
Watch out for: EC and non-EC tenants require different entity creation flows. Attempting to create EmpEmployment on a non-EC tenant will return an error. Confirm tenant type before integration design.
Delta sync of changed users for downstream system
- Store the timestamp of the last successful sync.
- Authenticate and GET /odata/v2/User?$filter=lastModified gt datetime'
'&$top=1000&$skip=0. - Iterate pages using $skip until results array is empty.
- For each changed user, apply updates to the downstream system.
- Update stored sync timestamp to current time after successful processing.
Watch out for: lastModified reflects changes to the User entity only; changes to related EC entities (EmpJob, EmpEmployment) have their own lastModifiedDateTime fields and must be queried separately.
Deactivate a terminated employee
- Authenticate and obtain Bearer token.
- PATCH /odata/v2/User('
') with body {"status":"inactive"} and X-HTTP-Method: MERGE header. - If Employee Central is enabled, also terminate the EmpEmployment record by setting endDate via PATCH on the EmpEmployment entity.
- Confirm deactivation: GET /odata/v2/User('
') and verify status=inactive.
Watch out for: Setting status=inactive does not automatically revoke SSO sessions or downstream system access. Coordinate with IdP session termination separately. Hard delete is not available via API.
Why building this yourself is a trap
The most common integration failure points are architectural, not syntactic. The dual entity model (User vs. PerPerson/EmpEmployment) means an integration built against one model will silently miss data from the other;
this is particularly acute for role, job, and employment data that lives exclusively in EC entities. Timestamp handling requires explicit OData v2 syntax (/Date(ms)/ epoch format on responses, datetime'...' literals in filters)-standard OData v4 or ISO 8601 assumptions will produce malformed queries or misparse responses. Rate limits are tenant-enforced and not publicly documented;
SAP returns HTTP 429 or 503 without standard retry headers, making exponential backoff and $batch usage non-optional for production workloads. Finally, the data center host is region-specific and not a universal endpoint, so hardcoding a base URL without tenant-region awareness will break cross-region deployments.
Automate SAP SuccessFactors 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.