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]
}
]
}'
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"])
{
"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"
}
}
{
"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"
}
{ "dry_run": true, "submitted": 2, "valid": 2 }
Create Commuting Periods (v2)
Create up to 5,000 commuting periods of your employees in one request; distances and emissions are calculated asynchronously
POST
/
v2
/
employees
/
commuting
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]
}
]
}'
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"])
{
"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"
}
}
{
"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"
}
{ "dry_run": true, "submitted": 2, "valid": 2 }
← Employees API
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 reads, in the vocabulary of
After the
POST /v1/employee-historic and with its rules.
The request checks every record and writes them all at once, then answers 202 Accepted with an
ingest job that calculates them (see
how bulk ingestion works). Follow it with
GET /v2/ingest-jobs/{id} or, for a job submitted with an API key, the
ingest_job.finished webhook. Like the rest of the bulk ingest API, this route
never sends an email.
Beta. Part of the bulk ingest API, in beta. The contract may still change before general availability.
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]
}
]
}'
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"])
{
"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"
}
}
{
"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"
}
{ "dry_run": true, "submitted": 2, "valid": 2 }
How it works
- 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_dateandend_date, stored or in the request. If any record is invalid the whole request is rejected with422and nothing is written;rejectedis the number of invalid records anderrorslists them (up to 100). The request never measures a distance. - The periods are written at once, each in the organization of its employee: the
x-organization-idorganization or one of its perimeter. - The job calculates them in the background, in chunks. A period sent without
total_kmgets the distance of the road or rail route (by its transport) between itsoriginanddestination, and is markedtotal_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 isuploaded. A job item isfailedwhen: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 withtotal_kmor other places.CALCULATION_FAILED: the period produced no emission at all (no factor for its commute or its days at home).
- When every chunk is done the job is
completed(orcompleted_with_errors). For a job submitted with an API key,ingest_job.finishedis sent to the webhook endpoints of thex-organization-idorganization.
How a period is counted
A period is the same commute repeated on theweekly_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
string
required
Your API key (see Authentication).
string
required
The organization you send as. Its employees, and those of the organizations under it that you can write to (its
perimeter), can be given periods. The job and its
ingest_job.finished webhook belong to this organization.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.Query Parameters
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
array[object]
required
The commuting periods to create, 1 to 5,000. A field not listed here is rejected (
SCHEMA_INVALID).Show record
Show record
uuid
required
The employee: an
id that Create Employees (v2) answers, or of
List Employees.date
required
First day of the period,
YYYY-MM-DD, from 1970 to next year.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.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).string
For a
car, and only for a car: small, medium or large.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.string
With
fuel_type: electric, and only with it: yes, no or do_not_know, whether the electricity is
renewable. Never for a bus.boolean
Whether the car is shared. Required for a
car; any other transport is false (sent or not).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.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.string
Where it ends, e.g. the office address, as
origin.integer
default:"1"
Trips each commuting day, 0 to 10 (1: one way).
array[integer]
Days of the week they commuted, each once:
0 (Monday) to 6 (Sunday). Required, and not empty, unless
telecommuting, which takes none.array[integer]
Days of the week they worked from home, as
weekly_travels, none of them in weekly_travels.integer
default:"8"
Hours worked a day, 0 to 24.
string
default:"active"
The employee in the period:
active, inactive (on leave) or terminated.uuid
A record of your custom emission factor databases to
calculate the commute with instead of the default: find it with
List Records. It must be visible from the employee’s
organization, enabled, and its
activity_categories must include employees_in_itinere.uuid
The same for the days worked from home: its
activity_categories must include employees_telecommute.string
Your own reference for the record, up to 255 characters, echoed in errors and in the job’s items.
Transport rules
The rules ofPOST /v1/employee-historic.
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}/itemshas one item per record, in the order sent. Itsentity_idis the period’s id, the one Delete Commuting Periods (v2) takes. Afaileditem carries itserror_code.- List Commuting Periods returns the periods with their distance
and emissions (
co2e).
If
chunks_done stops moving, see 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. Per-record codes inerrors[].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), 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 (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). |
202, a job item can end failed with DISTANCE_CALCULATION_ERROR or CALCULATION_FAILED (see
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.
Limits
| Limit | Value |
|---|---|
| Records per request | 1 to 5,000 |
| Periods per calculation chunk | 100 |
Related Endpoints
Create Employees (v2)
The employees, and their
employee_idDelete Commuting Periods (v2)
Delete periods by id
List Job Items
The period id and outcome of each record
List Custom Factor Records
The ids for
custom_ef_record_idWas this page helpful?