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

# V4 Work-Contracts to V5 Employments & Job-Positions

> Migration steps from /directory/api/4.0/work-contracts to the Lucca API employment and job-position resources.

The v4 work-contract API (`/directory/api/4.0/work-contracts`) is deprecated in favour of the **Lucca API** (v5), which replaces the
work-contract (representing the base contract) and work-contract-amendment (representing incremental historized changes to the base contract)
resources with different-typed and historized resources: `employment` and `job-position`.

<Warning>
  **This migration is only applicable to read-only integration scenarios.** The Lucca API `employment` and `job-position` resources are currently
  **read-only**. If your integration creates, updates, or deletes work-contracts, it cannot be migrated to the Lucca API at this time and you
  should keep using the v4 endpoint until a write API becomes available.
</Warning>

<Note>
  No sunset date has been announced for `/directory/api/4.0/work-contracts` yet. Make sure your API key has a valid contact email address so you
  receive deprecation notices when a date is set.
</Note>

<Tip>
  If your integration uses `/api/v3/users`, see the dedicated [users migration guide](/api-reference/legacy/directory/guides/users-migration).
</Tip>

***

## What Changes

In v4, a work-contract was a single record holding both the legal contract terms (start and end dates, contract type) and the employment
context (establishment, occupation category). The Lucca API splits this into dedicated resources:

<CardGroup cols={1}>
  <Card title="employment" icon="file-contract" horizontal>
    The legal contract: start/end dates, the legal entity, the contract template, and an optional external reference. One employment
    covers a continuous period with the company.
  </Card>

  <Card title="job-position" icon="briefcase" horizontal>
    The professional role during an employment: business establishment, department, manager, job title, occupation category, and more.
    One employment may span **multiple sequential job-positions** when the employee's role changes without the contract ending.
  </Card>

  <Card title="probationary-period" icon="hourglass" horizontal>
    The trial period associated with an employment, if any. It has its own start and end dates, which may differ from the employment's dates.
  </Card>

  <Card title="employee-attributes (unified read endpoint)" icon="list" horizontal>
    A uniform endpoint that surfaces any property from the employee, employment, and job-position resources — including custom extension
    fields — in a single call. The recommended approach for integrations that want a comprehensive snapshot of an employee's data without
    chaining multiple requests.
  </Card>
</CardGroup>

<Tip>
  **Recommended for bulk reads**: rather than querying `employments` and `job-positions` separately, use the
  [`employee-attributes`](/api-reference/latest/employee-attributes/get-started) endpoint. It lets you retrieve employment dates,
  business establishment, department, manager, and any other property in a **single paginated call** with
  `?definition.id=employment.start,jobPosition.department,...&applicability.asOf={TODAY}`. The `applicability` field on each attribute
  contains the time window during which the value applies, which replaces the v4 `isApplicable` fallback logic. The `employee-attributes`
  endpoint returns at most 100 items per page; for extension endpoints the limit is 1,000.
</Tip>

**Establishment and role fields moved to job-position**: in v4, fields like `establishmentId` and `spcId` sat directly on the work-contract. In v5, these belong to the **job-position**, not the employment. This means a new job-position is created whenever only the role (not the contract itself) changes — it is no longer necessary to terminate and re-create a contract for role-only changes.

**Snapshots replace diffs**: the v4 work-contract amendment model stored incremental changes on top of a base contract (a "diff" approach). In contrast, **employments and job-positions in v5 are "snapshot" resources** that each record the full state for their period — a job-position stores the complete establishment, occupation category, manager, etc., not just what changed.

***

## First: Switch to OAuth 2.0 and Set the Api-Version Header

First of all, replace your API key header with an OAuth 2.0 bearer token obtained from the Lucca authorization server. You will need to request
the appropriate scopes for the resources you intend to read:

| Resource                            | Required scope                            |
| :---------------------------------- | :---------------------------------------- |
| `employment`                        | `employments.readonly`                    |
| `job-position`                      | `job-positions.readonly`                  |
| `probationary-periods`              | `probationary-periods.readonly`           |
| `employee-attributes` (values)      | `employee-attributes.readonly`            |
| `employee-attributes` (definitions) | `employee-attribute-definitions.readonly` |

<Note>
  The `employee-attribute-definitions.readonly` scope is only needed if you want to **discover** attribute IDs programmatically (e.g. to enumerate
  all custom fields via `GET /lucca-api/employee-attribute-definitions`). If your integration already knows the definition IDs it needs,
  `employee-attributes.readonly` is sufficient.
</Note>

Refer to the [authentication guide](/documentation/using-api/authentication) for the full OAuth 2.0 flow.

Additionally, the v5 Lucca API is versioned, so you must include the `Api-Version: {VERSION}` HTTP header in all your requests. Currently, the
latest version is `2024-11-01`.

<CardGroup cols={1}>
  <Card title="Authentication" icon="key" cta="Read more" href="/documentation/using-api/authentication" horizontal>
    Read more about how to obtain and use OAuth 2.0 bearer tokens.
  </Card>

  <Card title="Versioning" icon="code-branch" cta="Read more" href="/documentation/using-api/versioning" horizontal>
    Read more about how to use the `Api-Version` header to specify the API version.
  </Card>
</CardGroup>

***

## Notable Breaking Changes

<Warning>
  Pay close attention to these semantic changes — they may cause silent data errors if overlooked.
</Warning>

### `establishmentId` moved to job-position

In v4, the establishment (`establishmentId`) was a property of the work-contract itself. In v5, it is a property
of the **job-position** (`businessEstablishment`). As a result, the same employment contract can now cover multiple
establishments across successive job-positions — you no longer need to terminate and re-create a contract simply
because the role changes.

### `legalEntity` vs. `businessEstablishment`

The v4 `establishmentId` maps to the v5 `jobPosition.businessEstablishment`. Meanwhile, `employment.legalEntity`
is a new concept in v5 with no direct v4 equivalent — it is the legal entity on the employment contract, inferred
from the first job-position's business establishment and displayed as "legal-unit" in the Lucca UI.

| V4 field             | V5 resource    | V5 field                | V5 concept                                                    |
| :------------------- | :------------- | :---------------------- | :------------------------------------------------------------ |
| `establishmentId`    | `job-position` | `businessEstablishment` | Where the employee physically works.                          |
| *(no v4 equivalent)* | `employment`   | `legalEntity`           | Legal entity (sometimes called "legal unit") on the contract. |

### Trial period day counts are gone

The v4 `trialPeriodDays` and `renewedTrialPeriodDays` integer fields have no v5 equivalent. Only the computed end dates
(`initialEndsOn` and `extendedEndsOn` on the `probationary-period` resource) are stored.

### Contract dates are now objects

The v4 `startsOn` and `endsOn` were plain date strings. In v5, `employment.start` and `employment.end` are **objects** with a nested
`date` sub-field:

```json theme={null}
// v4: plain string
"startsOn": "2024-01-01"

// v5: nested object
"start": { "date": "2024-01-01" }
```

This structural change will break any code that reads contract dates directly as strings. Update your parsing logic to extract the
`date` sub-field.

### IDs are strings in v5

All v5 resource identifiers are JSON strings (`"id": "514"`) rather than integers (`"id": 514`). Do not use strict
equality (`===`) between v4 integer IDs and v5 string IDs.

### Mapping v4 work-contract IDs to v5 employment IDs

<Warning>
  V4 `work-contract.id` and v5 `employment.id` are **not** an exact match, and as a result. If your integration depends on v4 work-contract IDs and you need to map them onto employments, then you should use the combination of the **employment start date** and the **employee's login** (`employee.remoteId`) as a unique identifier to match records. For a given employee, the pair `(employee.remoteId, employment.start.date)` should be unique and lets you correlate v4 and v5 data without relying on IDs.
</Warning>

Alternatively, if your v4 integration used `externalId`, that value is preserved as `employment.remoteId` in v5 — you can filter
employments by `remoteId` to find the corresponding v5 resource.

### `isApplicable` fallback is not replicated in v5 (breaking change)

<Warning>
  This change can **silently break** integrations that relied on the v4 fallback behavior. Test thoroughly.
</Warning>

In v4, the `isApplicable: true` flag used a fallback rule: if no contract was current, it marked the next upcoming contract as applicable,
or the last contract if there was none. The v5 `status: "active"` does **not** replicate this fallback — an employee with a gap between
employments will have no `active` employment during that gap. If your integration relied on this fallback, adjust your query logic accordingly.

**Recommended alternative**: use the [`employee-attributes`](/api-reference/latest/employee-attributes/get-started) endpoint with the
`applicability.asOf` query parameter. The `applicability` field on each returned attribute provides `start` and `end` dates that define
when the value applies. This gives you explicit temporal boundaries instead of the implicit v4 fallback logic. For example:

```http theme={null}
GET /lucca-api/employee-attributes?employee.id={ID}&definition.id=employment.start,employment.end&applicability.asOf={TODAY} HTTP/1.1
```

This returns only the attributes whose applicability period includes today's date, effectively giving you the "currently applicable"
employment data without relying on any fallback rule.

***

## Field Mapping Reference

<Note>
  Work-Contract = Employment + Job-Position + Employee-Attribute\[]
</Note>

| V4 work-contract field        | V5 resource                                            | V5 field                              | Notes                                                                                   |
| :---------------------------- | :----------------------------------------------------- | :------------------------------------ | :-------------------------------------------------------------------------------------- |
| `id`                          | `employment`                                           | `id`                                  | New ID; v4 and v5 IDs are not the same value — do not assume identity                   |
| `ownerId`                     | `employment` / `job-position`                          | `employee.id`                         | Both resources reference the employee                                                   |
| `externalId`                  | `employment`                                           | `remoteId`                            | Renamed; no uniqueness constraint in v5                                                 |
| `typeId`                      | `employment`                                           | `template.id`                         | Now a reference to the `employment-template` resource                                   |
| `startsOn`                    | `employment`                                           | `start.date`                          | Now a nested object: `{ "date": "YYYY-MM-DD" }`                                         |
| `endsOn`                      | `employment`                                           | `end.date`                            | Same wrapping object; `null` for open-ended contracts                                   |
| `isApplicable`                | `employment`                                           | `status`                              | `isApplicable: true` ≈ `status: "active"`. Read-only in both versions                   |
| `establishmentId`             | `job-position`                                         | `businessEstablishment.id`            | Moved from the contract to the job-position                                             |
| `spcId`                       | `job-position`                                         | `occupationCategory.id`               | Renamed; still references the same occupation categories                                |
| `trialPeriodEndDate`          | `probationary-period`                                  | `initialEndsOn`                       | Moved to the separate `probationary-period` resource                                    |
| `trialPeriodEndDate2`         | `probationary-period`                                  | `extendedEndsOn`                      | Same separate resource                                                                  |
| `trialPeriodDays`             | *(no equivalent)*                                      | —                                     | Only the end date is stored in v5; the day count is not                                 |
| `renewedTrialPeriodDays`      | *(no equivalent)*                                      | —                                     | Same — only the end date is stored                                                      |
| `hiringTypeId`                | `employee-attribute`                                   | `e_generated_hiringType`              | The hiring type is now stored as an employee attribute.                                 |
| `temporaryContractGroundId`   | `employee-attribute`                                   | `e_generated_temporaryContractGround` | The temporary contract ground is now stored as an employee attribute.                   |
| `internshipSupervisorId`      | `employee-attribute`                                   | `e_generated_internshipSupervisorId`  | The internship supervisor is now stored as an employee attribute.                       |
| `terminationReasonId`         | `employee-attribute`                                   | `e_generated_terminationReasonId`     | The termination reason is now stored as an employee attribute.                          |
| `createdAt`                   | `employment` or `job-position` or `employee-attribute` | `createdAt`                           | Same semantics                                                                          |
| `lastModifiedAt`              | `employment` or `job-position` or `employee-attribute` | `lastUpdatedAt`                       | Renamed                                                                                 |
| `authorId` / `lastModifierId` | *(no equivalent)*                                      | —                                     | Authorship metadata not exposed in v5 while there's no official "account" API resource. |

<Note>
  Fields migrated to `employee-attributes` with the `e_generated_` prefix are auto-generated attribute definitions created by Lucca when
  migrating legacy contract data. Confirm their exact IDs by querying `GET /lucca-api/employee-attribute-definitions`.
</Note>

<Tip>
  Fields that appeared on the v3 user object but were absent from the v4 work-contract — `department`, `manager`, `jobTitle`, `jobQualification` —
  are now in the **job-position** resource. Combine an employment query with a job-position query to get the full picture. Or retrieve
  everything in one call via the `employee-attributes` endpoint while making sure to set `?applicability.asOf={TODAY}`.
</Tip>

***

## Querying in V5

<Note>
  All collection endpoints enforce cursor-based pagination. Start with `?limit={page_size}&include=links`, then follow the `links.next` URL
  in each response to get the next page. Iterate until `links.next` is `null`. The maximum page size is 100 items (default 25).
</Note>

<Card title="Pagination" icon="forward" cta="Read more" href="/documentation/using-api/paging" horizontal>
  Learn how to paginate through collection endpoints in the Lucca API.
</Card>

### List all employments for an employee

<Note>
  Add `include=links` to the request — this is required to receive pagination links in the response. Iterate until `links.next` is `null` to retrieve all pages.
</Note>

<CodeGroup>
  ```http v4 theme={null}
  GET /directory/api/4.0/work-contracts?ownerId={EMPLOYEE_ID} HTTP/1.1
  Host: example.ilucca.net
  Authorization: lucca application={API_KEY}
  Accept: application/json
  ```

  ```http v5 theme={null}
  GET /lucca-api/employments?employee.id={EMPLOYEE_ID}&include=links HTTP/1.1
  Host: example.ilucca.net
  Authorization: Bearer {ACCESS_TOKEN}
  Api-Version: 2024-11-01
  Accept: application/json

  {
    "items": [
      {
        "id": "4561",
        "type": "employment",
        "employee": { "id": "416", "type": "employee", "url": "..." },
        "status": "active",
        "start": { "date": "2024-01-01" },
        "end": null,
        ...
      }
    ],
    "links": {
      "next": null
    }
  }
  ```
</CodeGroup>

<Tip>
  If you only need the currently active employment, add `&status=active` to the query parameters.
</Tip>

### Retrieve a single employment by ID

<CodeGroup>
  ```http theme={null}
  GET /lucca-api/employments/{EMPLOYMENT_ID} HTTP/1.1
  Host: example.ilucca.net
  Authorization: Bearer {ACCESS_TOKEN}
  Api-Version: 2024-11-01
  Accept: application/json

  {
    "id": "4561",
    "type": "employment",
    "url": "https://example.ilucca.net/lucca-api/employments/4561",
    "employee": { "id": "416", "type": "employee", "url": "..." },
    "legalEntity": { "id": "123", "type": "legal-entity", "url": "..." },
    "status": "active",
    "start": { "date": "2024-01-01" },
    "end": null,
    "template": { "id": "4", "type": "employment-template", "url": "..." },
    "remoteId": null,
    ...
  }
  ```
</CodeGroup>

### List all job-positions for an employment

<CodeGroup>
  ```http theme={null}
  GET /lucca-api/job-positions?employment.id={EMPLOYMENT_ID} HTTP/1.1
  Host: example.ilucca.net
  Authorization: Bearer {ACCESS_TOKEN}
  Api-Version: 2024-11-01
  Accept: application/json

  {
    "items": [
      {
        "id": "74",
        "type": "job-position",
        "employment": { "id": "4561", "type": "employment", "url": "..." },
        "employee": { "id": "416", "type": "employee", "url": "..." },
        "status": "active",
        "startsOn": "2024-01-01",
        "endsOn": null,
        "businessEstablishment": { "id": "5", "type": "business-establishment", "url": "..." },
        "jobTitle": "Developer",
        "manager": { "id": "541", "type": "employee", "url": "..." },
        "department": { "id": "32", "type": "department", "url": "..." },
        "occupationCategory": { "id": "12", "type": "occupation-category", "url": "..." },
        ...
      }
    ]
  }
  ```
</CodeGroup>

<Tip>
  To get only currently active job-positions, add `&status=active` to the query parameters.
  Also note that the `employment` can also be embedded inline using `?include=embedded` if you need employment
  details together with the job-position.
</Tip>

### Get the probationary period for an employment

<CodeGroup>
  ```http theme={null}
  GET /lucca-api/probationary-periods?employment.id={EMPLOYMENT_ID} HTTP/1.1
  Host: example.ilucca.net
  Authorization: Bearer {ACCESS_TOKEN}
  Api-Version: 2024-11-01
  Accept: application/json
  ```
</CodeGroup>

<Tip>
  If you need employment dates, business establishment, manager, department, and job title all in one call —
  rather than chaining separate requests to `employments` and `job-positions` — use the
  [`employee-attributes`](/api-reference/latest/employee-attributes/get-started) endpoint while making sure
  to set `applicability.asOf={TODAY}`.
</Tip>

***

## Further Reading

<CardGroup cols={2}>
  <Card title="Migrating from /api/v3/users" icon="user" cta="Read more" href="/api-reference/legacy/directory/guides/users-migration">
    Migration guide for the v3 user object, covering personal data, employment, and job-position fields.
  </Card>

  <Card title="Authentication" icon="key" cta="Read more" href="/documentation/using-api/authentication">
    How to obtain and use OAuth 2.0 bearer tokens.
  </Card>

  <Card title="Employment resource" icon="file-contract" cta="Read more" href="/api-reference/latest/employments/employment">
    Full reference for the v5 employment resource.
  </Card>

  <Card title="Job-position resource" icon="briefcase" cta="Read more" href="/api-reference/latest/employments/job-position">
    Full reference for the v5 job-position resource.
  </Card>

  <Card title="Get Started With Employee Attributes" icon="tags" cta="Read more" href="/api-reference/latest/employee-attributes/get-started">
    How to use the unified employee-attributes endpoint to read any employee property in a single call.
  </Card>
</CardGroup>
