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

# Create Workforce Employees (v2)

> Load up to 1,000 rows of the own workforce template in one request: employees, contracts and remunerations

[← Own Workforce API](/api-reference/own-workforce/overview)

Load your headcount from your HR system: employees, their contracts and what they earn. A record is a row of the
[own workforce import template](/api-reference/imports/own-workforce-templates): an employee, one of its contracts
and, optionally, one remuneration of that contract. Send one record per contract, or per remuneration of a
contract; the records of one employee share its `external_employee_id`.

The request applies the records as the file import does and answers `201 Created` with the employee of each record.
Nothing is calculated, so there is no job to follow.

<Warning>
  **Beta.** Part of the bulk ingest API, currently in beta. The contract may still change before general availability.
</Warning>

<RequestExample>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST "https://api.dcycle.io/v2/own-workforce/employees" \
    -H "x-api-key: ${DCYCLE_API_KEY}" \
    -H "x-organization-id: ${DCYCLE_ORG_ID}" \
    -H "Idempotency-Key: 2c1b0a99-8877-4665-9443-322110ffeedd" \
    -H "Content-Type: application/json" \
    -d '{
      "records": [
        {
          "external_employee_id": "HR-00412",
          "birth_date": "1988-04-12",
          "gender": "F",
          "disabled": "no_disability",
          "nationality": "ES",
          "location": "ES",
          "contract_type": "temporary",
          "contract_start_date": "2024-02-01",
          "contract_end_date": "2024-12-31",
          "contract_end_reason": "change_of_contract",
          "working_hours_ratio": 1,
          "labor_agreement": true,
          "employment_category": "Lab technician",
          "remuneration_amount": 28500,
          "remuneration_currency_unit_id": "d3e37f2b-0fc3-4532-82f8-3890ab56ad37",
          "remuneration_start_date": "2024-02-01",
          "client_row_id": "HR-00412/1"
        },
        {
          "external_employee_id": "HR-00412",
          "birth_date": "1988-04-12",
          "gender": "F",
          "disabled": "no_disability",
          "location": "ES",
          "contract_type": "permanent",
          "contract_start_date": "2025-01-01",
          "working_hours_ratio": 0.8,
          "labor_agreement": true,
          "employment_category": "Lab technician",
          "client_row_id": "HR-00412/2"
        }
      ]
    }'
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import os
  import uuid

  import requests

  response = requests.post(
      "https://api.dcycle.io/v2/own-workforce/employees",
      headers={
          "x-api-key": os.environ["DCYCLE_API_KEY"],
          "x-organization-id": os.environ["DCYCLE_ORG_ID"],
          "Idempotency-Key": str(uuid.uuid4()),  # keep it to retry this same batch
      },
      json={
          "records": [
              {
                  "external_employee_id": "HR-00412",
                  "birth_date": "1988-04-12",
                  "gender": "F",
                  "disabled": "no_disability",
                  "location": "ES",
                  "contract_type": "permanent",
                  "contract_start_date": "2025-01-01",
                  "working_hours_ratio": 0.8,
                  "labor_agreement": True,
                  "employment_category": "Lab technician",
              }
          ]
      },
      timeout=60,
  )
  response.raise_for_status()
  for item in response.json()["items"]:
      print(item["source_row_index"], item["employee"]["id"])
  ```
</RequestExample>

<ResponseExample>
  ```json 201 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "items": [
      {
        "source_row_index": 0,
        "client_row_id": "HR-00412/1",
        "employee": {
          "id": "6f5e4d3c-2b1a-4f09-8e7d-6c5b4a392817",
          "organization_id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
          "external_employee_id": "HR-00412",
          "birth_date": "1988-04-12",
          "gender": "F",
          "disabled": "no_disability",
          "nationality": "ES",
          "created_at": "2026-10-19T09:12:44.031Z",
          "updated_at": "2026-10-19T09:12:44.031Z"
        }
      },
      {
        "source_row_index": 1,
        "client_row_id": "HR-00412/2",
        "employee": { "id": "6f5e4d3c-2b1a-4f09-8e7d-6c5b4a392817", "...": "the same employee" }
      }
    ]
  }
  ```

  ```json 422 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "type": "https://code.dcycle.io/api-reference/errors#BULK_RECORDS_REJECTED",
    "title": "Unprocessable Entity",
    "status": 422,
    "code": "BULK_RECORDS_REJECTED",
    "detail": "1 of the submitted records are invalid; nothing was written.",
    "errors": [
      {
        "source_row_index": 1,
        "client_row_id": "HR-00412/2",
        "error_code": "CONTRACT_OVERLAP",
        "error_params": { "field": "contract_start_date" }
      }
    ],
    "rejected": 1,
    "request_id": "b2248a31-0f1b-4a7d-83ce-b29bd5977b4c"
  }
  ```

  ```json 200 (dry_run=true) theme={"theme":{"light":"github-light","dark":"github-dark"}}
  { "dry_run": true, "submitted": 2, "valid": 2 }
  ```
</ResponseExample>

## How it works

Each record is applied as the file import applies a row, in the order sent:

1. **The employee** is found by `external_employee_id` in the `x-organization-id` organization, or created with the
   record's personal data. An employee that already exists keeps its personal data (birth date, gender, disability,
   nationality): the record's are not applied.
2. **The contract** is identified by its start date.
   * A contract the employee already has with the same `contract_start_date` is updated with every field of the
     record except its dates.
   * A new contract is added. If the stored contract before it is open, that one is closed the day before the new
     one starts. If there is none before it but a stored one after, an open new contract ends the day before that
     one starts. Contracts sent in the same request are not closed for each other: send the end date of every
     contract but the employee's last, or the request answers `CONTRACT_OVERLAP`.
3. **The remuneration**, when sent, is identified by its start date within the contract. One that already exists is
   left as it is. A new one closes the open remuneration before it, as contracts do.

The request is all or nothing. Every record is checked, the resulting contracts and remunerations included: an
employee's contracts must not overlap, nor a contract's remunerations. If any record is invalid, the answer is
`422` listing every invalid record and nothing is written. Otherwise everything is written at once.

## Request

### Headers

<ParamField header="x-api-key" type="string" required>
  Your API key (see [Authentication](/api-reference/introduction#authentication)).
</ParamField>

<ParamField header="x-organization-id" type="string" required>
  The organization the employees belong to. A record cannot name another organization: send each organization's
  employees with its own `x-organization-id`.
</ParamField>

<ParamField header="Idempotency-Key" type="string" required>
  1 to 255 characters, unique per batch. A retry with the same key and body answers the same employees (with
  `Idempotent-Replayed: true`) instead of applying the records again; the same key with another body is
  `422 IDEMPOTENCY_KEY_REUSED`, and while the first request is still being processed `409 IDEMPOTENCY_KEY_IN_PROGRESS`.
</ParamField>

### Query Parameters

<ParamField query="dry_run" type="boolean" default="false">
  Check only: the same `422` for invalid records, nothing written; `200` with
  `{"dry_run": true, "submitted": n, "valid": n}` when every record is valid.
</ParamField>

### Body Parameters

<ParamField body="records" type="array[object]" required>
  The template rows, 1 to 1,000. A field not listed here is rejected (`SCHEMA_INVALID`). The codes below are the
  values of the template's columns; [Get Options Source](/api-reference/imports/get-options-source) lists them.

  <Expandable title="record">
    <ParamField body="external_employee_id" type="string" required>
      The employee's id in your HR system, up to 255 characters. It identifies the employee in the organization.
    </ParamField>

    <ParamField body="birth_date" type="date" required>
      `YYYY-MM-DD`, from 1900, not in the future.
    </ParamField>

    <ParamField body="gender" type="string" required>
      `M`, `F`, `O` (other) or `NS` (not stated).
    </ParamField>

    <ParamField body="disabled" type="string" required>
      The disability grade: `no_disability`, `with_disability`, `disability_33` or `disability_65`.
    </ParamField>

    <ParamField body="nationality" type="string">
      The employee's nationality, ISO 3166-1 alpha-2 (`ES`, `FR`...).
    </ParamField>

    <ParamField body="location" type="string" required>
      The country where the contract is worked, ISO 3166-1 alpha-2.
    </ParamField>

    <ParamField body="contract_type" type="string" required>
      `permanent` or `temporary`.
    </ParamField>

    <ParamField body="contract_start_date" type="date" required>
      First day of the contract. It identifies the contract within the employee.
    </ParamField>

    <ParamField body="contract_end_date" type="date">
      Last day of the contract, on or after its start. Omitted: the contract is open. Not applied to a contract
      that already exists.
    </ParamField>

    <ParamField body="contract_end_reason" type="string">
      Why the contract ended: `resignation`, `termination`, `change_of_contract`, `retirement` or `fatality`.
    </ParamField>

    <ParamField body="working_hours_ratio" type="number" required>
      Share of a full-time working day: over 0 and up to 1 (`0.5` is half time).
    </ParamField>

    <ParamField body="labor_agreement" type="boolean" required>
      Whether the contract is covered by a collective bargaining agreement.
    </ParamField>

    <ParamField body="employment_category" type="string" required>
      Your job category for the employee, as free text, up to 255 characters.
    </ParamField>

    <ParamField body="contract_annual_working_hours" type="number">
      Hours a year the contract establishes: over 0 and up to 2,928. On a contract that already exists, sending
      `null` clears it and omitting the field leaves it as it is.
    </ParamField>

    <ParamField body="remuneration_amount" type="number">
      What the employee earns in the remuneration's period, 0 or more. Send it with
      `remuneration_currency_unit_id` and `remuneration_start_date`.
    </ParamField>

    <ParamField body="remuneration_currency_unit_id" type="uuid">
      Euros: `d3e37f2b-0fc3-4532-82f8-3890ab56ad37`. Other currencies are not accepted yet.
    </ParamField>

    <ParamField body="remuneration_start_date" type="date">
      First day the remuneration applies. It identifies the remuneration within the contract.
    </ParamField>

    <ParamField body="remuneration_end_date" type="date">
      Last day the remuneration applies, on or after its start. Omitted: until further notice.
    </ParamField>

    <ParamField body="client_row_id" type="string">
      Your own reference for the record, up to 255 characters, echoed in errors and in the answer.
    </ParamField>
  </Expandable>
</ParamField>

## Response

`201 Created` with `items`: one per record, in the order sent, with its `source_row_index`, `client_row_id` and the
`employee` it was applied to. The records of one employee answer the same employee. Its `id` is the one
[Delete Workforce Employees (v2)](/api-reference/own-workforce/employees-delete-v2) takes. Read the contracts and
remunerations with [List Workforce Contracts](/api-reference/own-workforce/list-contracts).

## Errors

[Problem details](/api-reference/errors). Per-record codes in `errors[].error_code` of a `422 BULK_RECORDS_REJECTED`:

| `error_code` | Cause |
| - | - |
| `SCHEMA_INVALID` | The record does not match the schema: a missing or unknown field (e.g. `organization_id`), a code that is not one of the listed values (`gender`, `disabled`), `working_hours_ratio` out of range, an end date before its start, a remuneration without its amount, currency or start date... `error_params.errors` says where. |
| `REFERENCE_NOT_FOUND` | `location`, `nationality`, `contract_type` or `contract_end_reason` is not a known value. `error_params.field` says which. |
| `UNIT_NOT_SUPPORTED` | `remuneration_currency_unit_id` is not euros. |
| `CONTRACT_OVERLAP` | The record's contract would overlap another contract of the employee: one already stored, or one of another record. |
| `REMUNERATION_OVERLAP` | The record's remuneration would overlap another remuneration of the contract. |

The overlaps are checked once every record is otherwise valid.

Request-level errors: `401` for a missing or invalid API key, `403 LOGGED_USER_NOT_MEMBER` when the key's user is not a
member of the organization, `422 REQUEST_VALIDATION_FAILED` for a malformed body (no `records`, more than 1,000, no
`Idempotency-Key`), `422 IDEMPOTENCY_KEY_REUSED`, `409 IDEMPOTENCY_KEY_IN_PROGRESS`, and `429` beyond the
[rate limits](/api-reference/rate-limits).

## Limits

| Limit | Value |
| - | - |
| Records per request | 1 to 1,000 |

## Related Endpoints

<CardGroup cols={2}>
  <Card title="Delete Workforce Employees (v2)" icon="trash" href="/api-reference/own-workforce/employees-delete-v2">
    Delete employees by id
  </Card>

  <Card title="Own Workforce Import Templates" icon="table" href="/api-reference/imports/own-workforce-templates">
    The template the records follow
  </Card>

  <Card title="Get Options Source" icon="list" href="/api-reference/imports/get-options-source">
    The accepted codes
  </Card>

  <Card title="List Workforce Contracts" icon="file-contract" href="/api-reference/own-workforce/list-contracts">
    The contracts of an employee
  </Card>
</CardGroup>


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