Create Logistics Requests (Bulk, v2)
const options = {
method: 'POST',
headers: {
'x-api-key': '<x-api-key>',
'x-organization-id': '<x-organization-id>',
'Idempotency-Key': '<idempotency-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({records: [{client_row_id: '<string>'}]})
};
fetch('https://api.dcycle.io/v2/logistics/requests/bulk', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.dcycle.io/v2/logistics/requests/bulk"
payload = { "records": [{ "client_row_id": "<string>" }] }
headers = {
"x-api-key": "<x-api-key>",
"x-organization-id": "<x-organization-id>",
"Idempotency-Key": "<idempotency-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)curl --request POST \
--url https://api.dcycle.io/v2/logistics/requests/bulk \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--header 'x-api-key: <x-api-key>' \
--header 'x-organization-id: <x-organization-id>' \
--data '
{
"records": [
{
"client_row_id": "<string>"
}
]
}
'Create Logistics Requests (Bulk, v2)
Create up to 5,000 logistics legs in one request; their distance and emissions are calculated by an ingest job
POST
/
v2
/
logistics
/
requests
/
bulk
Create Logistics Requests (Bulk, v2)
const options = {
method: 'POST',
headers: {
'x-api-key': '<x-api-key>',
'x-organization-id': '<x-organization-id>',
'Idempotency-Key': '<idempotency-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({records: [{client_row_id: '<string>'}]})
};
fetch('https://api.dcycle.io/v2/logistics/requests/bulk', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.dcycle.io/v2/logistics/requests/bulk"
payload = { "records": [{ "client_row_id": "<string>" }] }
headers = {
"x-api-key": "<x-api-key>",
"x-organization-id": "<x-organization-id>",
"Idempotency-Key": "<idempotency-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)curl --request POST \
--url https://api.dcycle.io/v2/logistics/requests/bulk \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--header 'x-api-key: <x-api-key>' \
--header 'x-organization-id: <x-organization-id>' \
--data '
{
"records": [
{
"client_row_id": "<string>"
}
]
}
'← Logistics API
Creates up to 5,000 logistics legs (shipments) in one request. The legs are written at once and their distance and
emissions are calculated asynchronously by an ingest job: the answer is the job
(
Legs are upserted on their business key (
With
Other codes from the distance engine may appear; they are stable and come with a
Requests over the cap get HTTP
202, processing), never the legs.
- All or nothing. If any record is invalid the answer is
422 BULK_RECORDS_REJECTED, listing every invalid record, and nothing is written: no leg, package or hub. - Idempotent.
Idempotency-Keyis required: retrying with the same key and body returns the original job. - Per-leg results. A leg whose distance or emissions could not be calculated is a failed item of the job, with
its reason (
error_code), and stays stored without emissions.
Request
Headers
string
required
Your API key. Jobs submitted with an API key notify
ingest_job.finished to your
webhook endpoints.string
required
UUID of the organization the legs belong to.
string
required
A unique value per batch (max 255 characters). Retrying with the same key and body returns the original job instead
of writing the legs again; the same key with another body is rejected (
IDEMPOTENCY_KEY_REUSED).Query Parameters
boolean
default:"false"
Validate only: the same checks and
422, but nothing is written and no job is created. The Idempotency-Key is
still required but not recorded.Body Parameters
array
required
The legs to create, 1 to 5,000. Each record has the fields of
Create Logistics Request, plus:
Show extra field
Show extra field
string
Your key for the record (max 255 characters), echoed on its job item and in a
422.movement_id, movement_stretch, movement_stage, client and
package_key): sending a leg again in a later batch updates it in place, clears its emissions and recalculates it. Two records of the
same batch with the same key are rejected (DUPLICATE_RECORD). A hub_id that names no hub of the organization
creates it.
Response
202 Accepted
The job, withLocation: /v2/ingest-jobs/{id}.
{
"id": "4f0b8b3e-6a41-4b7e-9d0c-2c1f3a5e8d10",
"status": "processing",
"entity_type": "logistics",
"operation": "create",
"source": "api_bulk",
"counts": {"submitted": 5000, "succeeded": 0, "failed": 0},
"chunks_total": 10,
"chunks_done": 0,
"created_at": "2026-10-13T08:00:00Z",
"finished_at": null,
"links": {
"self": "/v2/ingest-jobs/4f0b8b3e-6a41-4b7e-9d0c-2c1f3a5e8d10",
"items": "/v2/ingest-jobs/4f0b8b3e-6a41-4b7e-9d0c-2c1f3a5e8d10/items"
}
}
dry_run=true and every record valid: 200 with {"dry_run": true, "submitted": 5000, "valid": 5000}.
Job status
PollGET /v2/ingest-jobs/{id} or subscribe to ingest_job.finished. The job ends
completed, or completed_with_errors when some legs could not be calculated.
GET /v2/ingest-jobs/{id}/items maps each record (source_row_index,
client_row_id) to its leg (entity_id) and its result.
Why a leg failed to calculate
A failed item’serror_code says why, and error_params.message explains it. The leg stays stored without
emissions; fix the cause and send it again (same business key: it is updated in place).
error_code | Cause | Fix |
|---|---|---|
NO_DISTANCE | No distance could be computed between origin and destination (e.g. an address that cannot be located) | Send a more precise address, or distance_km |
SEA_CROSSING_REQUIRES_MARITIME_MODE | Origin and destination are separated by sea, so the leg cannot go by road | Declare a maritime toc/category, or split the leg |
NO_LOAD | The leg lost its load after it was accepted (e.g. edited meanwhile) | Send it again with load |
NO_TOC | The leg lost its transport operation category after it was accepted | Send it again with toc or category |
NOT_FOUND | The leg was deleted before it was calculated | Send it again if you still need it |
message.
Errors
422 BULK_RECORDS_REJECTED — invalid records
Nothing was written.rejected is how many records are invalid and errors lists them (up to 100) in submission
order, with source_row_index, your client_row_id and a stable error_code.
{
"type": "https://code.dcycle.io/api-reference/errors#BULK_RECORDS_REJECTED",
"title": "Unprocessable Content",
"status": 422,
"code": "BULK_RECORDS_REJECTED",
"rejected": 2,
"errors": [
{"source_row_index": 1, "client_row_id": "row-2", "error_code": "INVALID_LOAD",
"error_params": {"detail": "Load cannot be negative or 0"}},
{"source_row_index": 3, "client_row_id": "row-4", "error_code": "DUPLICATE_RECORD",
"error_params": {"duplicate_of": 0}}
],
"request_id": "b2248a31-0f5e-4a54-9f0a-3d1f2b6c7e90"
}
error_code | Cause | Fix |
|---|---|---|
SCHEMA_INVALID | The record does not match the schema, e.g. a movement_id, movement_stretch, movement_stage, client, package_key, hub_id or license plate over 255 characters or with a NUL character; error_params.errors lists each field (loc, msg) | Fix the fields listed |
INVALID_LOAD | load is 0 or negative | Send a positive load, or omit it to use the TOC’s default |
INVALID_DISTANCE | distance_km is 0 or negative | Send a positive distance_km, or omit it to have it computed |
ACTIVITY_DATE_OUT_OF_RANGE | shipment_date is before 1970 or after next year | Fix the date |
TOC_NOT_FOUND | No TOC matches toc/category in the origin’s region (error_params: toc, category, region) | Use a TOC from List TOCs |
LOAD_NOT_AVAILABLE | No load, and the TOC has no default load | Send load |
ADDRESS_TOO_LONG | origin or destination is over 500 characters (error_params: field, length, max) | Send the address alone |
DUPLICATE_RECORD | Another record of the batch is the same leg (error_params.duplicate_of: its index) | Send each leg once |
422 IDEMPOTENCY_KEY_REUSED
TheIdempotency-Key was already used with a different body. Use a new key for a new batch.
422 REQUEST_VALIDATION_FAILED — malformed request
A missingIdempotency-Key, a body without records, more than 5,000 records. See Errors.
401 Unauthorized / 403 Forbidden
A missing or invalid API key, or a user that is not a member ofx-organization-id.
Examples
Create a batch and wait for the calculation
Python
import os
import time
import uuid
import requests
BASE = "https://api.dcycle.io"
headers = {
"x-api-key": os.environ["DCYCLE_API_KEY"],
"x-organization-id": os.environ["DCYCLE_ORG_ID"],
# One key per batch: reuse it only to retry this same batch.
"Idempotency-Key": str(uuid.uuid4()),
}
records = [
{
"origin": "Madrid, Spain",
"destination": "Barcelona, Spain",
"origin_country": "ES",
"load": 1000,
"load_unit": "kg",
"toc": "van_diesel",
"movement_id": "SHIP-001",
"client_row_id": "row-1",
}
]
response = requests.post(f"{BASE}/v2/logistics/requests/bulk", headers=headers, json={"records": records})
if response.status_code == 422 and response.json()["code"] == "BULK_RECORDS_REJECTED":
for rejection in response.json()["errors"]:
print(rejection["source_row_index"], rejection["client_row_id"], rejection["error_code"])
raise SystemExit(1)
response.raise_for_status()
job_url = BASE + response.headers["Location"]
while (job := requests.get(job_url, headers=headers).json())["status"] == "processing":
time.sleep(5)
failed = requests.get(f"{job_url}/items", headers=headers, params={"status": "failed"}).json()
for item in failed["items"]:
print("not calculated:", item["client_row_id"], item["error_code"])
Retries and idempotency
Retry a timed-out or failed request with the sameIdempotency-Key and body: if the first attempt was accepted
you get its job back, and nothing is written twice. A new batch needs a new key.
Limits
| Limit | Value |
|---|---|
| Records per request | 5,000 |
| Maximum concurrent requests | 10 per API key, per endpoint |
| Recommended concurrency | 8 (leaving headroom under the limit) |
429 with a Retry-After header. The cap is on requests in flight at once, not on
requests per second — see Rate Limits.
Related Endpoints
Get Ingest Job
Follow the calculation
Create Request (v2)
Create a single request
List Requests
Retrieve requests with filtering and pagination
List Packages
View aggregated package emissions
Was this page helpful?