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

> Create up to 5,000 commuting periods of your employees in one request; distances and emissions are calculated asynchronously

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

Say how your employees got to work (Scope 3 category 7, employee commuting): one record per employee and period, with
the transport, the days of the week they commuted or worked from home, and how far, or between which places. Dcycle
works out the distance when you leave it out and calculates the emissions in the background. The periods are the ones
[List Commuting Periods](/api-reference/employees/commuting-periods/list) reads, in the vocabulary of
[`POST /v1/employee-historic`](/api-reference/employees/commuting-periods/create) and with its rules.

The request checks every record and writes them all at once, then answers `202 Accepted` with an
[ingest job](/api-reference/ingest-jobs/get) that calculates them (see
[how bulk ingestion works](/api-reference/ingest-jobs/overview)). Follow it with
[`GET /v2/ingest-jobs/{id}`](/api-reference/ingest-jobs/get) or, for a job submitted with an API key, the
[`ingest_job.finished`](/api-reference/webhooks/overview) webhook. Like the rest of the bulk ingest API, this route
**never sends an email**.

> [!WARNING]
>
> **Beta.** Part of the bulk ingest API, in beta. The contract may still change before general availability.

```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "https://api.dcycle.io/v2/employees/commuting" \
  -H "x-api-key: ${DCYCLE_API_KEY}" \
  -H "x-organization-id: ${DCYCLE_ORG_ID}" \
  -H "Idempotency-Key: 6f5e4d3c-2b1a-4f09-8e7d-6c5b4a392817" \
  -H "Content-Type: application/json" \
  -d '{
    "records": [
      {
        "employee_id": "4c3b2a19-0f8e-4d7c-9b6a-5f4e3d2c1b0a",
        "start_date": "2026-01-01",
        "end_date": "2026-06-30",
        "transport_type": "car",
        "vehicle_size": "medium",
        "fuel_type": "diesel",
        "carpool": false,
        "origin": "Calle de Alcalá 1, Madrid",
        "destination": "Paseo de la Castellana 100, Madrid",
        "weekly_travels": [0, 1, 2],
        "weekly_telecommuting": [3, 4],
        "client_row_id": "HR-1042-2026H1"
      },
      {
        "employee_id": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "start_date": "2026-01-01",
        "end_date": "2026-06-30",
        "transport_type": "train",
        "fuel_type": "electric",
        "renewable_energy": "yes",
        "total_km": 32.5,
        "weekly_travels": [0, 1, 2, 3, 4]
      }
    ]
  }'
```

```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/commuting",
    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": [
            {
                "employee_id": "4c3b2a19-0f8e-4d7c-9b6a-5f4e3d2c1b0a",
                "start_date": "2026-07-01",
                "end_date": "2026-12-31",
                "transport_type": "telecommuting",
                "weekly_telecommuting": [0, 1, 2, 3, 4],
                "client_row_id": "HR-1042-2026H2",
            }
        ]
    },
    timeout=60,
)
response.raise_for_status()
job = response.json()
print(job["id"], job["status"])
```

```json 202 theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7081",
  "status": "processing",
  "entity_type": "employee_commuting",
  "operation": "create",
  "source": "api_bulk",
  "counts": { "submitted": 2, "succeeded": 0, "failed": 0 },
  "chunks_total": 1,
  "chunks_done": 0,
  "created_at": "2026-10-20T09:12:44.031Z",
  "finished_at": null,
  "links": {
    "self": "/v2/ingest-jobs/1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7081",
    "items": "/v2/ingest-jobs/1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7081/items"
  }
}
```

```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-2026H1",
      "error_code": "COMMUTING_PERIOD_ALREADY_EXISTS",
      "error_params": {
        "field": "start_date",
        "period_id": "8e7d6c5b-4a39-4281-8f7e-6d5c4b3a2918"
      }
    }
  ],
  "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 }
```

## How it works

1. **Every record is checked**: its fields and the transport rules, its employee, its custom factors, and that the
   employee has no period with the same `start_date` and `end_date`, stored or in the request. If any record is invalid
   the **whole** request is rejected with `422` and nothing is written; `rejected` is the number of invalid records and
   `errors` lists them (up to 100). The request never measures a distance.
2. **The periods are written at once**, each in the organization of its employee: the `x-organization-id` organization
   or one of its [perimeter](/api-reference/ingest-jobs/overview#perimeter).
3. **The job calculates them** in the background, in chunks. A period sent without `total_km` gets the distance of the
   road or rail route (by its transport) between its `origin` and `destination`, and is marked `total_km_estimated`.
   Then its commute is calculated under employee commuting, and its days at home under telecommuting where there is a
   factor for them. An employee with a calculated period is `uploaded`. A job item is `failed` when:
   * `DISTANCE_CALCULATION_ERROR`: a place could not be found or no route joins them. The period is kept without a
     distance and is not calculated: delete it and send it again with `total_km` or other places.
   * `CALCULATION_FAILED`: the period produced no emission at all (no factor for its commute or its days at home).
4. **When every chunk is done** the job is `completed` (or `completed_with_errors`). For a job submitted with an API
   key, `ingest_job.finished` is sent to the webhook endpoints of the `x-organization-id` organization.

### How a period is counted

A period is the same commute repeated on the `weekly_travels` days between `start_date` and `end_date`, without the
public holidays of the organization's country: `daily_trips` trips each of those days, over `total_km`. A shared car
(`carpool: true`) divides the emissions. The `weekly_telecommuting` days count as days worked from home. A change of
address or transport is a new period, with its own dates.

## Request

### Headers

**x-api-key** `string` *required*

Your API key (see [Authentication](/api-reference/introduction#authentication)).

**x-organization-id** `string` *required*

The organization you send as. Its employees, and those of the organizations under it that you can write to (its
[perimeter](/api-reference/ingest-jobs/overview#perimeter)), can be given periods. The job and its
`ingest_job.finished` webhook belong to this organization.

**Idempotency-Key** `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 returns the original job (with `Idempotent-Replayed: true`) instead of
creating the periods 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).

### Query Parameters

**dry\_run** `boolean` (default: false)

Check only: the same `422` for invalid records, nothing written, no job; `200` with
`{"dry_run": true, "submitted": n, "valid": n}` when every record is valid. It does not check or store the
`Idempotency-Key`.

### Body Parameters

**records** `array[object]` *required*

The commuting periods to create, 1 to 5,000. A field not listed here is rejected (`SCHEMA_INVALID`).

**record**

**employee\_id** `uuid` *required*

The employee: an `id` that [Create Employees (v2)](/api-reference/employees/create-v2) answers, or of
[List Employees](/api-reference/employees/list).

**start\_date** `date` *required*

First day of the period, `YYYY-MM-DD`, from 1970 to next year.

**end\_date** `date` *required*

Last day of the period, on or after `start_date`, from 1970 to next year. An employee has one period per
`start_date` and `end_date`.

**transport\_type** `string` *required*

How they commuted: `bicycle`, `bus`, `car`, `electric_kick_scooter`, `metro`, `motorbike`, `telecommuting`
(they work from home), `train`, `tram`, `trolleybus` or `walking`. It decides which of `vehicle_size`,
`fuel_type`, `renewable_energy` and `carpool` the record takes (see [Transport rules](#transport-rules)).

**vehicle\_size** `string`

For a `car`, and only for a car: `small`, `medium` or `large`.

**fuel\_type** `string`

What the vehicle runs on: `petrol`, `diesel`, `electric`, `hybrid`, `lpg`, `natural_gas`, `not_fuel_based` or
`do_not_know`, among those its transport takes.

**renewable\_energy** `string`

With `fuel_type: electric`, and only with it: `yes`, `no` or `do_not_know`, whether the electricity is
renewable. Never for a `bus`.

**carpool** `boolean`

Whether the car is shared. Required for a `car`; any other transport is `false` (sent or not).

**total\_km** `number`

One-way distance of the commute, in km, 0 or more. Omitted: the job works it out from `origin` and
`destination`. A `telecommuting` period takes none.

**origin** `string`

Where the commute starts, e.g. the home address, 1 to 500 characters. Send it with `destination`; required with
it when there is no `total_km`. With `total_km`, the two are stored as they are and nothing is measured. A
`telecommuting` period takes none.

**destination** `string`

Where it ends, e.g. the office address, as `origin`.

**daily\_trips** `integer` (default: 1)

Trips each commuting day, 0 to 10 (1: one way).

**weekly\_travels** `array[integer]`

Days of the week they commuted, each once: `0` (Monday) to `6` (Sunday). Required, and not empty, unless
`telecommuting`, which takes none.

**weekly\_telecommuting** `array[integer]`

Days of the week they worked from home, as `weekly_travels`, none of them in `weekly_travels`.

**hours\_worked\_per\_day** `integer` (default: 8)

Hours worked a day, 0 to 24.

**situation** `string` (default: active)

The employee in the period: `active`, `inactive` (on leave) or `terminated`.

**custom\_ef\_record\_id** `uuid`

A record of your [custom emission factor databases](/api-reference/custom-emission-factors/overview) to
calculate the commute with instead of the default: find it with
[List Records](/api-reference/custom-emission-factors/list-records). It must be visible from the employee's
organization, enabled, and its `activity_categories` must include `employees_in_itinere`.

**telecommute\_custom\_ef\_record\_id** `uuid`

The same for the days worked from home: its `activity_categories` must include `employees_telecommute`.

**client\_row\_id** `string`

Your own reference for the record, up to 255 characters, echoed in errors and in the job's items.

### Transport rules

The rules of [`POST /v1/employee-historic`](/api-reference/employees/commuting-periods/create#transport-validation-rules).
A field a transport does not take is left out (or `null`); sending it is `SCHEMA_INVALID`, except for `walking` and
`telecommuting`, which ignore any vehicle detail.

| `transport_type` | `vehicle_size` | `fuel_type` | `renewable_energy` | `carpool` |
| - | - | - | - | - |
| `car` | `small`, `medium`, `large` | `diesel`, `natural_gas`, `petrol`, `lpg`, `electric`, `hybrid`, `do_not_know` | with `electric` | required |
| `motorbike` | none | `diesel`, `petrol`, `electric`, `do_not_know` | with `electric` | `false` |
| `bus` | none | `diesel`, `natural_gas`, `petrol`, `lpg`, `do_not_know` | none | `false` |
| `train` | none | `diesel`, `electric`, `do_not_know` | with `electric` | `false` |
| `metro`, `trolleybus` | none | `electric`, `do_not_know` | with `electric` | `false` |
| `bicycle` | none | `electric`, `not_fuel_based`, `do_not_know` | with `electric` | `false` |
| `tram`, `electric_kick_scooter`, `walking`, `telecommuting` | none | none | none | `false` |

## Response

`202 Accepted` with the job (`entity_type: employee_commuting`, `operation: create`) and a `Location` header to it. To
read the result:

* [`GET /v2/ingest-jobs/{id}/items`](/api-reference/ingest-jobs/list-items) has one item per record, in the order
  sent. Its `entity_id` is the period's id, the one
  [Delete Commuting Periods (v2)](/api-reference/employees/commuting-delete-v2) takes. A `failed` item carries its
  `error_code`.
* [List Commuting Periods](/api-reference/employees/commuting-periods/list) returns the periods with their distance
  and emissions (`co2e`).

> [!WARNING]
>
> If `chunks_done` stops moving, see [A job that does not finish](/api-reference/ingest-jobs/get#a-job-that-does-not-finish):
> never resend the batch with a new `Idempotency-Key`, because its periods are already stored (and would answer
> `COMMUTING_PERIOD_ALREADY_EXISTS`).

## 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` or `commuting_type`), a field its transport does not take (see [Transport rules](#transport-rules)), a car without `carpool`, no `total_km` and no `origin` + `destination`, only one of them, a distance or places on a `telecommuting` period, no `weekly_travels` on another transport, a repeated day, a day in both `weekly_travels` and `weekly_telecommuting`, `end_date` before `start_date`, a year out of range... `error_params.errors` says where. |
| `REFERENCE_NOT_FOUND` | `employee_id` does not exist, is disabled or is outside your organization's [perimeter](/api-reference/ingest-jobs/overview#perimeter) (all answer the same). `error_params.field` is `employee_id`. |
| `COMMUTING_PERIOD_ALREADY_EXISTS` | The employee already has a period with the same `start_date` and `end_date`. `error_params.period_id` is its id: delete it first to replace it. |
| `DUPLICATE_COMMUTING_PERIOD` | Another record of the request is for the same employee, `start_date` and `end_date`. |
| `MULTIDB_CUSTOM_RECORD_NOT_FOUND_OR_DISABLED` | `custom_ef_record_id` or `telecommute_custom_ef_record_id` (`error_params.field` says which) does not exist, is disabled or is not visible from the employee's organization. |
| `MULTIDB_CUSTOM_RECORD_INCOMPATIBLE_CATEGORY` | That custom record is not configured for its activity category (`employees_in_itinere` or `employees_telecommute`). |

After the `202`, a job item can end `failed` with `DISTANCE_CALCULATION_ERROR` or `CALCULATION_FAILED` (see
[How it works](#how-it-works)).

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 5,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 5,000 |
| Periods per calculation chunk | 100 |

## Related Endpoints

> **[Create Employees (v2)](/api-reference/employees/create-v2)**
>
> The employees, and their `employee_id`

> **[Delete Commuting Periods (v2)](/api-reference/employees/commuting-delete-v2)**
>
> Delete periods by id

> **[List Job Items](/api-reference/ingest-jobs/list-items)**
>
> The period id and outcome of each record

> **[List Custom Factor Records](/api-reference/custom-emission-factors/list-records)**
>
> The ids for `custom_ef_record_id`


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