> ## 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 Employees (v2)

> Create up to 1,000 employees of the commuting survey in one request

[← Employees API](/api-reference/employees/overview)

Add employees to the directory of the commuting survey: a name, an email or both, and whether they still work there.
Nothing is calculated, so the request is synchronous (see
[how bulk ingestion works](/api-reference/ingest-jobs/overview)): it answers `201 Created` with the created employees.
It also records an [ingest job](/api-reference/ingest-jobs/list), born `completed`, that sends no webhook.

An employee has no emissions until their commuting periods are known: send them with
[Create Commuting Periods (v2)](/api-reference/employees/commuting-create-v2), or collect them with the commuting
survey. This route **never sends an email**: no survey goes out when an employee is created.

<Warning>
  **Beta.** Part of the bulk ingest API, 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/employees" \
    -H "x-api-key: ${DCYCLE_API_KEY}" \
    -H "x-organization-id: ${DCYCLE_ORG_ID}" \
    -H "Idempotency-Key: 8a7b6c5d-4e3f-4a21-9b0c-1d2e3f4a5b6c" \
    -H "Content-Type: application/json" \
    -d '{
      "records": [
        {
          "email": "ana.garcia@example.com",
          "name": "Ana García",
          "situation": "active",
          "client_row_id": "HR-1042"
        }
      ]
    }'
  ```

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

  import requests

  response = requests.post(
      "https://api.dcycle.io/v2/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": [
              {
                  "email": "ana.garcia@example.com",
                  "name": "Ana García",
                  "situation": "active",
                  "client_row_id": "HR-1042",
              }
          ]
      },
      timeout=60,
  )
  response.raise_for_status()
  print([item["employee"]["id"] for item in response.json()["items"]])
  ```
</RequestExample>

<ResponseExample>
  ```json 201 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "items": [
      {
        "source_row_index": 0,
        "client_row_id": "HR-1042",
        "employee": {
          "id": "4c3b2a19-0f8e-4d7c-9b6a-5f4e3d2c1b0a",
          "organization_id": "a8315ef3-dd50-43f8-b7ce-d839e68d51fa",
          "email": "ana.garcia@example.com",
          "name": "Ana García",
          "situation": "active",
          "status": "loading",
          "created_at": "2026-10-20T09:12:44.031Z"
        }
      }
    ]
  }
  ```

  ```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": 0,
        "client_row_id": "HR-1042",
        "error_code": "EMPLOYEE_ALREADY_EXISTS",
        "error_params": {
          "field": "email",
          "value": "ana.garcia@example.com",
          "employee_id": "4c3b2a19-0f8e-4d7c-9b6a-5f4e3d2c1b0a"
        }
      }
    ],
    "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": 1, "valid": 1 }
  ```
</ResponseExample>

## How it works

1. **Every record is checked**: it has an email, a name or both, and its email is free. An email is unique in the
   organization, without regard to case: one that an employee of the organization already has, or that another record
   of the request has, rejects the record. If any record is invalid the **whole** request is rejected with `422` and
   nothing is created ([all or nothing](/api-reference/ingest-jobs/overview#all-or-nothing)): `rejected` is the number
   of invalid records and `errors` lists them (up to 100).
2. **Every employee is created in one transaction** and returned, in the order sent, with `status: loading` (as the
   app's import does) until their commute is known. `source_row_index` is the position of the record in `records`, and
   `client_row_id` echoes yours.
3. **The employees belong to the `x-organization-id` organization.** A record cannot name another one, not even one of
   your [perimeter](/api-reference/ingest-jobs/overview#perimeter): send each organization's employees with its own
   `x-organization-id`.

An employee without an email is not matched against the ones already stored: sending the same name twice creates two
employees. To retry a request safely, repeat it with the same `Idempotency-Key`.

## 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.
</ParamField>

<ParamField header="Idempotency-Key" type="string" required>
  1 to 255 characters, one per batch, looked up per organization across every v2 bulk route. Within 24 hours, the
  same key with the same route and body answers the same employees (with `Idempotent-Replayed: true`) instead of
  creating them again; the same key with another body or on another route is `422 IDEMPOTENCY_KEY_REUSED`, and while
  the first request is still being processed `409 IDEMPOTENCY_KEY_IN_PROGRESS`. After 24 hours the key is free, and a
  request with it is a new one. See [The Idempotency-Key](/api-reference/ingest-jobs/overview#the-idempotency-key).
</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. It does not check or store the
  `Idempotency-Key`.
</ParamField>

### Body Parameters

<ParamField body="records" type="array[object]" required>
  The employees to create, 1 to 1,000. A field not listed here is rejected (`SCHEMA_INVALID`).

  <Expandable title="record">
    <ParamField body="email" type="string">
      Their email, up to 255 characters, unique in the organization (case does not matter). Required if `name` is not
      sent.
    </ParamField>

    <ParamField body="name" type="string">
      Their name, 1 to 255 characters. Required if `email` is not sent.
    </ParamField>

    <ParamField body="situation" type="string" default="active">
      `active`, `inactive` (on leave) or `terminated` (no longer works there).
    </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
created `employee` (`id`, `organization_id`, `email`, `name`, `situation`, `status`, `created_at`). Its `id` is the one
[Delete Employees (v2)](/api-reference/employees/delete-v2) takes.

## 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: neither `email` nor `name`, an email that is not valid, an empty `name`, a `situation` not listed, an unknown field (e.g. `organization_id` or `status`)... `error_params.errors` says where. |
| `EMPLOYEE_ALREADY_EXISTS` | An employee of the organization already has this email (case aside). `error_params.employee_id` is theirs. |
| `DUPLICATE_EMAIL` | Another record of the request has the same email (case aside). |

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 Employees (v2)" icon="trash" href="/api-reference/employees/delete-v2">
    Delete employees by id
  </Card>

  <Card title="List Employees" icon="users" href="/api-reference/employees/list">
    The employees stored, and their ids
  </Card>

  <Card title="Resend Commuting Survey" icon="envelope" href="/api-reference/employees/resend-survey">
    Send the survey, on request
  </Card>

  <Card title="List Ingest Jobs" icon="list-check" href="/api-reference/ingest-jobs/list">
    The job each request records
  </Card>
</CardGroup>


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