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

> Create up to 5,000 freight shipments in one request; distances and emissions are calculated asynchronously

[← Transport API](/api-reference/transport/overview)

Create one or many freight shipments (Scope 3 categories 4 and 9, upstream and downstream transportation). Say when
each shipment was made, in which direction, how much it weighed and its legs (sections), each with its transport and
either its distance or the places it joins; Dcycle works out the distance when you leave it out, picks the emission
factor and calculates the emissions in the background. The shipments are the ones
[`/v1/transports`](/api-reference/transport/list) reads and edits.

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.

<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/transports" \
    -H "x-api-key: ${DCYCLE_API_KEY}" \
    -H "x-organization-id: ${DCYCLE_ORG_ID}" \
    -H "Idempotency-Key: 9c8b7a69-5847-4e36-b251-a4f3e2d1c0b9" \
    -H "Content-Type: application/json" \
    -d '{
      "records": [
        {
          "transport_date": "2026-03-04",
          "transport_direction": "downstream",
          "quantity_transported": 12.5,
          "unit_id": "cab66828-c2b1-431b-92af-f9ab37149d3c",
          "supplier": "LogiTrans S.A.",
          "sections": [
            {
              "transport_type": "road",
              "travel_method": "truck",
              "origin": "Madrid, Spain",
              "destination": "Valencia Port, Spain"
            },
            {
              "transport_type": "maritime",
              "distance_km": 2950
            }
          ],
          "client_row_id": "SHIP-2026-0311"
        },
        {
          "transport_date": "2026-03-05",
          "transport_direction": "upstream",
          "distance_only": true,
          "sections": [
            { "transport_type": "road", "travel_method": "car", "distance_km": 42 }
          ]
        }
      ]
    }'
  ```

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

  import requests

  response = requests.post(
      "https://api.dcycle.io/v2/transports",
      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": [
              {
                  "transport_date": "2026-03-04",
                  "transport_direction": "upstream",
                  "quantity_transported": 1500,
                  "unit_id": "61743a63-ff70-459c-9567-5eee8f7dfd5c",
                  "sections": [
                      {
                          "transport_type": "do_not_know",
                          "origin": "Lyon, France",
                          "destination": "Zaragoza, Spain",
                      }
                  ],
                  "client_row_id": "SHIP-2026-0312",
              }
          ]
      },
      timeout=60,
  )
  response.raise_for_status()
  job = response.json()
  print(job["id"], job["status"])
  ```
</RequestExample>

<ResponseExample>
  ```json 202 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "id": "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
    "status": "processing",
    "entity_type": "transport_routes",
    "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/2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
      "items": "/v2/ingest-jobs/2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d/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": "SHIP-2026-0311",
        "error_code": "TRANSPORT_COMBINATION_INVALID",
        "error_params": { "field": "sections[1]" }
      }
    ],
    "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

1. **Every record is checked**: its fields, its weight unit, each section's transport against the
   [combinations](/api-reference/transport/combinations) there are emission factors for, and its custom factors. If
   any 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 looks up no place: an address that cannot be found only shows
   in the job.
2. **The shipments are written at once**, with their sections and status `pending`, in the `x-organization-id`
   organization.
3. **The job calculates them** in the background, in chunks. A section sent without `distance_km` gets the distance
   between its origin and destination for its transport (road or rail route, sea route for `maritime`, great-circle
   for `air`), and its places are located. A `do_not_know` section is
   [inferred](/api-reference/transport/do-not-know-inference) as for `POST /v1/transports`: road by truck within a
   country when there is a road route, air otherwise. Then each section gets the emission factor of its transport,
   its distance and the shipment's weight. Each shipment ends `active` or `error`. Its job item is then `failed` with
   the reason when there is one:
   * `DISTANCE_CALCULATION_ERROR`: the distance of a section could not be had (an address that cannot be found, no
     route for that transport between the two places, or the distance provider unavailable). The shipment is not
     calculated: send it again with the places fixed, or with `distance_km`.
   * `CALCULATION_FAILED`: no emission factor for a section, or another calculation error.
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.

### Coming from `POST /v1/transports`

The record is the shipment v1 takes, with these differences:

* `transport_direction` is required (v1 assumes `downstream` when it is missing).
* A section gives its distance as `distance_km`, instead of `kms_manual` with `distance_manual: true`. A section
  without it gives `origin` and `destination`.
* There is no `detail`: the distance or weight band of the factor is picked from the section's distance and the
  shipment's weight, as v1 does when `detail` is left out.
* There is no `organization_id`, nor any other field not listed below: the shipments belong to the
  `x-organization-id` organization.

## 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 shipments belong to. A record cannot name another organization: send each organization's
  shipments with its own `x-organization-id`. The job and its `ingest_job.finished` webhook belong to that
  organization too, so register a [webhook endpoint](/api-reference/webhooks/overview#groups-of-organizations) in each
  organization you send as.
</ParamField>

<ParamField header="Idempotency-Key" type="string" required>
  1 to 255 characters, unique per batch. A retry with the same key and body returns the original job
  (with `Idempotent-Replayed: true`) instead of creating the shipments 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, no job; `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 shipments to create, 1 to 5,000. A field not listed here is rejected (`SCHEMA_INVALID`).

  <Expandable title="record">
    <ParamField body="transport_date" type="date" required>
      Day of the shipment, `YYYY-MM-DD`, from 1970 to next year.
    </ParamField>

    <ParamField body="transport_direction" type="string" required>
      `upstream` for goods you buy (category 4) or `downstream` for goods you sell (category 9).
    </ParamField>

    <ParamField body="sections" type="array[object]" required>
      The legs of the shipment, in order, 1 to 20.

      <Expandable title="section">
        <ParamField body="transport_type" type="string" required>
          `road`, `rail`, `maritime`, `air`, or `do_not_know` to let the job infer road or air from the places.
        </ParamField>

        <ParamField body="travel_method" type="string">
          The road vehicle, only for `road`: `truck`, `car`, `motorbike`, `bicycle` or `electric_kick_scooter`.
        </ParamField>

        <ParamField body="electric" type="boolean" default="false">
          The vehicle is electric.
        </ParamField>

        <ParamField body="refrigerated" type="boolean" default="false">
          The goods travel refrigerated.
        </ParamField>

        <ParamField body="distance_km" type="number">
          Distance of the leg, in km, from 0.01 to 500,000. Omitted: calculated by the job from `origin` and
          `destination`. Not taken by `do_not_know`, whose transport depends on the places.
        </ParamField>

        <ParamField body="origin" type="string">
          Where the leg starts: an address, a city, a port, a postal code, 1 to 500 characters. Send it with
          `destination` when there is no `distance_km`.
        </ParamField>

        <ParamField body="destination" type="string">
          Where the leg ends, as `origin`.
        </ParamField>

        <ParamField body="custom_ef_record_id" type="uuid">
          A record of your [custom emission factor databases](/api-reference/custom-emission-factors/overview) to
          calculate the leg with instead of the default: find it with
          [List Records](/api-reference/custom-emission-factors/list-records). Its `activity_categories` must include
          the shipment's direction: `transport_distribution_upstream` or `transport_distribution_downstream`.
        </ParamField>
      </Expandable>

      The `transport_type`, `travel_method`, `refrigerated` and `electric` of a section must be one of the
      [transport combinations](/api-reference/transport/combinations) (`TRANSPORT_COMBINATION_INVALID` otherwise).
    </ParamField>

    <ParamField body="quantity_transported" type="number">
      Weight carried, in `unit_id`, over 0 and up to 999,999,999.99999. Required unless `distance_only`.
    </ParamField>

    <ParamField body="unit_id" type="uuid">
      The unit of `quantity_transported`: a weight unit, one of those
      [`GET /v2/units?type=transport`](/api-reference/units/list) lists (kilogram
      `61743a63-ff70-459c-9567-5eee8f7dfd5c`, metric tonne `cab66828-c2b1-431b-92af-f9ab37149d3c`, pound). Required
      unless `distance_only`.
    </ParamField>

    <ParamField body="distance_only" type="boolean" default="false">
      Calculate from the distance alone, with per vehicle-km factors, when the weight is not known: `road` sections
      only, and no `quantity_transported` or `unit_id`.
    </ParamField>

    <ParamField body="name" type="string">
      A label for the shipment, up to 255 characters.
    </ParamField>

    <ParamField body="supplier" type="string">
      The carrier, up to 255 characters.
    </ParamField>

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

## Response

`202 Accepted` with the job (`entity_type: transport_routes`, `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 shipment's id. A `failed` item carries its `error_code`.
* [List Transport Routes](/api-reference/transport/list) returns the shipments with their status and emissions
  (`co2e`); [Get Transport Route](/api-reference/transport/get) adds each section's distance and errors.

## Errors

[Problem details](/api-reference/errors). Per-record codes in `errors[].error_code` of a `422 BULK_RECORDS_REJECTED`
(each invalid record is listed once, with its first error):

| `error_code` | Cause |
| - | - |
| `SCHEMA_INVALID` | The record does not match the schema: a missing or unknown field (e.g. `organization_id`, `kms_manual`, `detail`), no `transport_direction`, no sections or more than 20, a section with no `distance_km` and no `origin` + `destination`, a `do_not_know` section with `distance_km`, no `quantity_transported` or `unit_id` without `distance_only`, a weight or a section other than `road` with `distance_only`, a date out of range... `error_params.errors` says where. |
| `UNIT_NOT_SUPPORTED` | `unit_id` is not a weight unit a shipment takes (`error_params.field` is `unit_id`). |
| `TRANSPORT_COMBINATION_INVALID` | A section's `transport_type`, `travel_method`, `refrigerated` and `electric` are not one of the [transport combinations](/api-reference/transport/combinations). `error_params.field` names the section, e.g. `sections[1]`. |
| `MULTIDB_CUSTOM_RECORD_NOT_FOUND_OR_DISABLED` | A section's `custom_ef_record_id` does not exist, is disabled or is not visible from your organization (`error_params.field` is e.g. `sections[0].custom_ef_record_id`). |
| `MULTIDB_CUSTOM_RECORD_INCOMPATIBLE_CATEGORY` | A section's `custom_ef_record_id` is not configured for the shipment's direction (`transport_distribution_upstream` or `transport_distribution_downstream`). |

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 |
| Sections per shipment | 1 to 20 |
| Shipments per calculation chunk | 50 |

## Related Endpoints

<CardGroup cols={2}>
  <Card title="Delete Freight Transports (v2)" icon="trash" href="/api-reference/transport/delete-v2">
    Delete shipments by id
  </Card>

  <Card title="Transport Combinations" icon="shuffle" href="/api-reference/transport/combinations">
    The section transports there are factors for
  </Card>

  <Card title="Get Ingest Job" icon="magnifying-glass" href="/api-reference/ingest-jobs/get">
    Follow the calculation
  </Card>

  <Card title="List Job Items" icon="list" href="/api-reference/ingest-jobs/list-items">
    The shipment id and outcome of each record
  </Card>

  <Card title="List Transport Routes" icon="table" href="/api-reference/transport/list">
    The shipments and their emissions
  </Card>

  <Card title="List Units" icon="ruler" href="/api-reference/units/list">
    The weight units a shipment takes
  </Card>
</CardGroup>


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