> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# SAP SuccessFactors

> Client-mode REST API connector for SAP SuccessFactors: send clock-in and clock-out punches from shop-floor terminals to Time Tracking, record and cancel absences as EmployeeTime, and read users, job information, departments, locations, time types and time account balances over OData v2, authenticated with OAuth 2.0 SAML bearer tokens

Technical Preview

## Overview

A client-mode connector for [SAP SuccessFactors](https://help.sap.com/docs/successfactors-platform/sap-successfactors-api-reference-guide-odata-v2/about-sap-successfactors-odata-apis-v2) HXM: the OData v2 API (User Management, Employee Central employment and personal information, Foundation Objects, Time Off) and the Time Tracking Clock In Clock Out APIs. Use it to connect the shop floor to HR:

* Badge readers and gate terminals publish clock-in / clock-out punches on MQTT, and the connector sends them to Clock In Clock Out.
* A kiosk or HMI books or cancels an absence, or shows the remaining vacation balance.
* Line planning and andon boards read who is assigned to a plant and department, with standard hours (EmpJob with `asOfDate`).

The connector `SapSuccessFactorsRestApi` in `SapSuccessfactorsRestApi.lot` has 28 events (23 GET, 5 POST). 3 are writes: `EmployeeTimeCreate`, `Upsert`, `TimeEventsCreate`. There are no DELETE, PUT or PATCH events.

Set `BASE_ADDRESS` to the API server of your data center, with no trailing slash and no `/odata/v2`, for example `https://api55.sapsf.eu` or `https://api4.successfactors.com` (SAP KBA 2215682 lists all API servers). Topics use `finance/sap-successfactors/<object>/<verb>`; replies land on `<topic>/response`.

See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes).

## Setup

1. Create or choose an API user with role-based permissions for the entities the broker uses (for example User, Employee Central job information, Time Off) and **Employee Central API** / **SFAPI** access. Every call runs with this user's permissions.
2. In **Admin Center > Manage OAuth2 Client Applications**, register a client: application name and URL, **Bind to Users** with the API user, and **Generate X.509 Certificate**. Download the certificate (it holds the private key) and save the **API Key** (the `client_id`).
3. For Clock In Clock Out punches, register a second client bound to the technical user `TECHNICAL_USER_CICOTERMINAL_9D3F8AC1`. SuccessFactors accepts punches only with that user's token (SAP KBA 3206348).
4. Note your **company ID**. Keep `SF_COMPANY_ID`, `SF_CLIENT_ID`, the private key or assertions, and the current token in broker secrets and inject them with `GET SECRET`.

## Authentication

SuccessFactors uses the OAuth 2.0 SAML 2.0 bearer assertion grant:

1. **Get a SAML assertion** for the API user, signed with the client's private key. Generate it outside the broker (for example with SAP's assertion generator or an integration platform), or use `SamlAssertion` (`POST /oauth/idp` with `client_id`, `user_id`, `token_url`, `private_key`). SAP has deprecated `/oauth/idp` because the private key travels in the call (KBA 3239495); it still answers today, but prefer generating the assertion yourself.
2. **Get a token** with `OAuthToken`: `POST /oauth/token` with `company_id`, `client_id` and `assertion` (the grant type `urn:ietf:params:oauth:grant-type:saml2-bearer` is fixed in the template). The reply has `access_token`, `token_type` `Bearer` and `expires_in`.
3. Every other event sends `Authorization: Bearer <access_token>` from the payload. `TokenValidate` checks a token.

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"company_id": "<SF_COMPANY_ID>", "client_id": "<SF_CLIENT_ID>", "assertion": "<URL-encoded SAML assertion>"}
```

The token events send a form body, so every value must be URL-encoded. Assertions and private keys are Base64 and contain `+`, `/` and `=`: send them as `%2B`, `%2F` and `%3D`, or the server rejects the assertion.

> [!WARNING]
>
> HTTP Basic authentication (`user@companyId:password`) for SuccessFactors APIs reached end of maintenance on 2 June 2025 and is deleted on 13 November 2026 (SAP KBA 3472405), so this template does not use it. If you already use OpenID Connect through SAP Cloud Identity Services, get the token from your Identity Services tenant and send it as `access_token` the same way. Never retain topics that carry an assertion, private key or token, and restrict `finance/sap-successfactors/auth/#` with ACLs.

## Events

| Event | Request | Source topic | Notes |
| - | - | - | - |
| SamlAssertion | `POST /oauth/idp` | `finance/sap-successfactors/auth/assertion` | Generate a SAML 2.0 assertion on the server (deprecated by SAP, KBA 3239495: the X.509 private key travels in the call; prefer signing the assertion yourself). |
| OAuthToken | `POST /oauth/token` | `finance/sap-successfactors/auth/token` | Exchange a SAML assertion for an access token (grant\_type urn:ietf:params:oauth:grant-type:saml2-bearer, fixed). |
| TokenValidate | `GET /oauth/validate` | `finance/sap-successfactors/auth/validate` | Check that access\_token is still valid (401 when it is malformed or expired). Use before a burst of calls or after a 401. |
| Metadata | `GET /odata/v2/{entity_set}/$metadata` | `finance/sap-successfactors/metadata/get` | OData v2 metadata (EDMX XML) of one entity set, for example User, EmpJob or EmployeeTime, including custom fields of your instance. Accept application/xml. |
| EntityQuery | `GET /odata/v2/{entity_set}?{query}` | `finance/sap-successfactors/odata/query` | Generic read of any entity set your API user may read (entity\_set e.g. PerEmail, FOCostCenter, WorkSchedule, Holiday). |
| UsersList | `GET /odata/v2/User?{query}` | `finance/sap-successfactors/users/list` | List users (catalog event). |
| UserGet | `GET /odata/v2/User('{user_id}')?{query}` | `finance/sap-successfactors/users/get` | One user by userId (catalog event), for a badge-to-person lookup. |
| PerPersonList | `GET /odata/v2/PerPerson?{query}` | `finance/sap-successfactors/persons/list` | List persons (biographical information). |
| PerPersonGet | `GET /odata/v2/PerPerson('{person_id}')?{query}` | `finance/sap-successfactors/persons/get` | One person by personIdExternal. |
| EmpEmploymentList | `GET /odata/v2/EmpEmployment?{query}` | `finance/sap-successfactors/employments/list` | Employments (hire, termination, assignment). |
| EmpJobList | `GET /odata/v2/EmpJob?{query}` | `finance/sap-successfactors/jobs/list` | Job information (catalog event): department, location, manager, standard hours, time recording variant. Effective dated: add asOfDate=YYYY-MM-DD or fromDate / toDate. |
| EmpJobGet | `GET /odata/v2/EmpJob(seqNumber={seq_number}L,startDate=datetime'{start_date}',userId='{user_id}')?{query}` | `finance/sap-successfactors/jobs/get` | One job information record by its key: seq\_number (number), start\_date (YYYY-MM-DDT00:00:00), user\_id. |
| FODepartmentList | `GET /odata/v2/FODepartment?{query}` | `finance/sap-successfactors/departments/list` | Departments (Foundation Object, effective dated). |
| FOLocationList | `GET /odata/v2/FOLocation?{query}` | `finance/sap-successfactors/locations/list` | Locations, such as plants and sites (Foundation Object, effective dated). |
| EmployeeTimeList | `GET /odata/v2/EmployeeTime?{query}` | `finance/sap-successfactors/employee-time/list` | Absences and other employee time records. |
| EmployeeTimeGet | `GET /odata/v2/EmployeeTime('{external_code}')?{query}` | `finance/sap-successfactors/employee-time/get` | One EmployeeTime by externalCode, for example to read approvalStatus after a create. |
| EmployeeTimeCreate | `POST /odata/v2/EmployeeTime` | `finance/sap-successfactors/employee-time/create` | Write. Create an EmployeeTime record, such as an absence (catalog event POST EmployeeTime). |
| Upsert | `POST /odata/v2/upsert?{query}` | `finance/sap-successfactors/odata/upsert` | Write. OData v2 upsert: create or update one record (or an array) of any upsertable entity set, named in \_\_metadata.uri. |
| TimeTypeList | `GET /odata/v2/TimeType?{query}` | `finance/sap-successfactors/time-types/list` | Time types (vacation, sickness, shift types) with their codes for EmployeeTime. |
| AvailableTimeTypeList | `GET /odata/v2/AvailableTimeType?{query}` | `finance/sap-successfactors/time-types/available` | Time types available to employees through their time profile. |
| TimeAccountList | `GET /odata/v2/TimeAccount?{query}` | `finance/sap-successfactors/time-accounts/list` | Time accounts (vacation entitlement, overtime) per user. |
| TimeAccountDetailList | `GET /odata/v2/TimeAccountDetail?{query}` | `finance/sap-successfactors/time-accounts/details` | Bookings on time accounts (accruals, absences, payouts). |
| EmpTimeAccountBalanceList | `GET /odata/v2/EmpTimeAccountBalance?{query}` | `finance/sap-successfactors/time-accounts/balances` | Current balance per time account, for a kiosk or HMI that shows remaining vacation. |
| EmployeeTimeCalendarList | `GET /odata/v2/EmployeeTimeCalendar?{query}` | `finance/sap-successfactors/employee-time/calendar` | Day-by-day calendar entries of employee time records. |
| TimeEventsCreate | `POST /rest/timemanagement/timeeventprocessing/v1/TimeEvents` | `finance/sap-successfactors/clock/events/create` | Write. Send clock-in / clock-out punches from a terminal to Clock In Clock Out (catalog clock events). The token must belong to an OAuth client bound to TECHNICAL\_USER\_CICOTERMINAL\_9D3F8AC1. |
| TimeEventsExport | `GET /odatav4/timemanagement/timeeventprocessing/clockinclockout/v1/timeevents?{query}` | `finance/sap-successfactors/clock/events/list` | Export clock events (OData v4, up to 1000 per call; needs the export permission, else an empty list). |
| ClockGroupsList | `GET /odatav4/timemanagement/timeeventprocessing/ClockInClockOutIntegration.svc/v1/ClockInClockOutGroups?{query}` | `finance/sap-successfactors/clock/groups/list` | Clock In Clock Out groups with their time event types (codes for typeCode). |
| ClockGroupGet | `GET /odatav4/timemanagement/timeeventprocessing/ClockInClockOutIntegration.svc/v1/ClockInClockOutGroups('{group_code}')?{query}` | `finance/sap-successfactors/clock/groups/get` | One Clock In Clock Out group by code. |

## Payloads

| Field | Used by | Content |
| - | - | - |
| `company_id`, `client_id`, `assertion` | `OAuthToken` | Company ID, API key of the OAuth client and the SAML assertion, URL-encoded |
| `client_id`, `user_id`, `token_url`, `private_key` | `SamlAssertion` | API key, API user (or `TECHNICAL_USER_CICOTERMINAL_9D3F8AC1`), `https%3A%2F%2F<api-server>%2Foauth%2Ftoken` and the private key, URL-encoded |
| `access_token` | every event except the two token events | Token from `OAuthToken` |
| `user_id`, `person_id`, `external_code`, `group_code` | `UserGet`, `PerPersonGet`, `EmployeeTimeGet`, `ClockGroupGet` | Key of the record |
| `seq_number`, `start_date`, `user_id` | `EmpJobGet` | EmpJob key: sequence number, start date `YYYY-MM-DDT00:00:00`, user ID |
| `entity_set` | `EntityQuery`, `Metadata` | Any OData v2 entity set, for example `PerEmail`, `FOCostCenter`, `EmployeeTime` |
| `query` | read events, `Upsert` | URL query string without `?`, or `""`, with spaces as `%20`, for example `$filter=userId%20eq%20'cgrant'&$top=50` |
| `body` | `EmployeeTimeCreate`, `Upsert`, `TimeEventsCreate` | JSON object (or array for `Upsert`); a JSON array of punches for `TimeEventsCreate` |

OData v2 query options: `$filter`, `$select`, `$expand`, `$orderby`, `$top`, `$skip`, `$format`, `$inlinecount`, and the SuccessFactors effective-dating options `asOfDate`, `fromDate` and `toDate` (`YYYY-MM-DD`). Dates in OData v2 JSON bodies use `/Date(<epoch milliseconds>)/`.

## Basic Syntax

An excerpt of the template (the full file has all 28 events and comments):

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SapSuccessFactorsRestApi WITH TYPE REST_API
    ADD REST_API_CONFIG
        WITH BASE_ADDRESS "https://YOUR_API_SERVER"
        WITH ENABLE_CLIENT "true"
        WITH USE_SSL "true"
        WITH REQUEST_TIMEOUT '120'
        WITH DEFAULT_CONTENT_TYPE "application/json"
    ADD EVENT OAuthToken
        WITH SOURCE_TOPIC "finance/sap-successfactors/auth/token"
        WITH DESTINATION_TOPIC "finance/sap-successfactors/auth/token/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/oauth/token"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)"
            ADD "Content-Type" "application/x-www-form-urlencoded"
        WITH BODY "company_id={value.json.company_id}&client_id={value.json.client_id}&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Asaml2-bearer&assertion={value.json.assertion}"
    ADD EVENT EmpJobList
        WITH SOURCE_TOPIC "finance/sap-successfactors/jobs/list"
        WITH DESTINATION_TOPIC "finance/sap-successfactors/jobs/list/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/odata/v2/EmpJob?{value.json.query}"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)"
            ADD "Authorization" "Bearer {value.json.access_token}"
    ADD EVENT EmployeeTimeCreate
        WITH SOURCE_TOPIC "finance/sap-successfactors/employee-time/create"
        WITH DESTINATION_TOPIC "finance/sap-successfactors/employee-time/create/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/odata/v2/EmployeeTime"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    ADD EVENT Upsert
        WITH SOURCE_TOPIC "finance/sap-successfactors/odata/upsert"
        WITH DESTINATION_TOPIC "finance/sap-successfactors/odata/upsert/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/odata/v2/upsert?{value.json.query}"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    ADD EVENT TimeEventsCreate
        WITH SOURCE_TOPIC "finance/sap-successfactors/clock/events/create"
        WITH DESTINATION_TOPIC "finance/sap-successfactors/clock/events/create/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/rest/timemanagement/timeeventprocessing/v1/TimeEvents"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
```

## Examples

### Keep a token for the other actions

Publish a fresh, URL-encoded assertion from your signer to `state/sf/assertion` (assertions are short-lived). A `COLLAPSED` model builds the token request, because `PUBLISH TOPIC` does not take inline JSON:

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SfTokenRequest COLLAPSED
    ADD STRING "company_id"
    ADD STRING "client_id"
    ADD STRING "assertion"

DEFINE ACTION SfToken
ON TOPIC "state/sf/assertion" DO
    SET "company" WITH GET SECRET "SF_COMPANY_ID"
    SET "client" WITH GET SECRET "SF_CLIENT_ID"
    SET "assertion" WITH PAYLOAD AS STRING
    PUBLISH MODEL SfTokenRequest TO "finance/sap-successfactors/auth/token" WITH
        company_id = {company}
        client_id = {client}
        assertion = {assertion}

DEFINE ACTION SfKeepToken
ON TOPIC "finance/sap-successfactors/auth/token/response" DO
    SET "token" WITH (GET JSON "access_token" IN PAYLOAD AS STRING)
    KEEP TOPIC "state/sf/access_token" WITH {token}
```

### Who works where today

Every hour, read the job information valid today for one plant, with department, manager and standard hours. The reply is OData v2 JSON: rows in `d.results`; follow `d.__next` (a `$skiptoken` link) for the next page.

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SfQuery COLLAPSED
    ADD STRING "access_token"
    ADD STRING "query"

DEFINE ACTION SfShiftRoster
ON EVERY 60 MINUTES DO
    SET "token" WITH (GET TOPIC "state/sf/access_token" AS STRING)
    PUBLISH MODEL SfQuery TO "finance/sap-successfactors/jobs/list" WITH
        access_token = {token}
        query = "$filter=location%20eq%20'PORTO'&$select=userId,jobTitle,department,location,standardHours,managerId&$top=200"
```

Without `asOfDate` SuccessFactors returns the record valid today; add `asOfDate=2026-10-05` (or `fromDate` / `toDate`) for another day.

### Send badge punches to Clock In Clock Out

The badge gateway publishes the punch payload on `finance/sap-successfactors/clock/events/create` (a LoT model cannot build the JSON array that the API expects). `access_token` must come from the client bound to `TECHNICAL_USER_CICOTERMINAL_9D3F8AC1`. `P10` / `P20` stand for the clock-in and clock-out time event types of your Clock In Clock Out group; `ClockGroupsList` lists them.

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<CICO access_token>", "body": [{"id": "1", "assignmentId": "cgrant", "typeCode": "P10", "timestamp": "2026-10-05T07:58:00+01:00", "terminalId": "PORTO-GATE-1", "timeTypeCode": "WORKING_TIME"}]}
```

SuccessFactors answers `201` with `succeededTimeEvents` (one `UUID` per punch) and `failedTimeEvents` (for example `A time event for this employee and timestamp already exists.`). Up to 1000 punches can go in one call; `id` only has to be unique within the call. Read them back with `TimeEventsExport`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<access_token>", "query": "$filter=workAssignmentId%20eq%20'cgrant'%20and%20timestampUTC%20ge%202026-10-05T00:00:00Z&$orderby=timestampUTC&$top=100"}
```

### Book and cancel an absence

Publish to `finance/sap-successfactors/employee-time/create` (the time type must exist and be allowed for the employee; check with `TimeTypeList` / `AvailableTimeTypeList`):

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<access_token>", "body": {"externalCode": "CF-0001", "userId": "cgrant", "timeType": "TT_VAC_REC", "startDate": "/Date(1791763200000)/", "endDate": "/Date(1791763200000)/", "comment": "Booked from the plant kiosk"}}
```

Cancel it later with an upsert on `finance/sap-successfactors/odata/upsert`. The reply has one row per record with `status` `OK` or `ERROR` and a `message`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<access_token>", "query": "$format=json", "body": {"__metadata": {"uri": "EmployeeTime('CF-0001')"}, "externalCode": "CF-0001", "approvalStatus": "CANCELLED"}}
```

## Notes

* Effective-dated entities (EmpJob, EmpEmployment, Foundation Objects) return the record valid today unless you pass `asOfDate`, or `fromDate` / `toDate` for history.
* `EmpJob` cannot be created with POST; it is upsert only (the metadata marks it `upsertable`, not `creatable`). `Upsert` works for any upsertable entity, but HR master data changes usually belong in SuccessFactors itself; the template only shows time records.
* `TimeEventsExport` needs the export permission; without it SuccessFactors returns an empty list with `200`.
* Use `Metadata` (`/odata/v2/<entity>/$metadata`) to see the custom fields of your instance, and `EntityQuery` to read entity sets that have no dedicated event.
* Errors: OData v2 returns `401` with `[LGN0007]Invalid OAuth token` for a bad token; the Clock In Clock Out APIs answer `401` `AuthenticationRequired` without a token and `400` `AuthenticationFailed` for a malformed one; `/oauth/token` answers `400` with `errorHttpCode` / `errorMessage`.
* Not packaged on purpose: every DELETE, the Clock In Clock Out mass delete (`PATCH` with `deleted: true`), PUT / MERGE updates (use `Upsert`), `$batch`, SFAPI and Compound Employee, the OData v4 Employee Central APIs, and HTTP Basic authentication.

## Status

Technical preview, blocked on a live test. Static, spec, example and mock checks pass, and keyless calls to real SuccessFactors API servers return the documented `400` / `401` answers. A live run needs a SuccessFactors test instance (company ID and API server), an OAuth2 client with its X.509 key bound to an API user, and a second client bound to the Clock In Clock Out technical user for punches.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.