> ## 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 Logistics Requests (Bulk)

> Process multiple logistics shipments in a single request with optimized performance

# Create Logistics Requests (Bulk)

Process multiple logistics requests in a single API call. Optimized for high-volume scenarios like daily batch uploads from logistics providers.

<Note>
  **New API**: This endpoint is part of the new API architecture with improved design and maintainability.
</Note>

## Key Features

<CardGroup cols={2}>
  <Card title="High Volume" icon="layer-group">
    Process up to **5,000 records** per request
  </Card>

  <Card title="Optimized Performance" icon="bolt">
    Internal caching of TOCs, hubs, and distances for faster processing
  </Card>

  <Card title="Error Handling" icon="shield-check">
    Continue processing even if some records fail with `continue_on_error`
  </Card>

  <Card title="Same Schema" icon="copy">
    Uses the same record format as the single request endpoint
  </Card>
</CardGroup>

## Request

### Headers

<ParamField header="x-api-key" type="string" required>
  Your API key for authentication

  **Example:** `sk_live_1234567890abcdef`
</ParamField>

<ParamField header="x-organization-id" type="string" required>
  Your organization UUID

  **Example:** `a8315ef3-dd50-43f8-b7ce-d839e68d51fa`
</ParamField>

### Body Parameters

<ParamField body="records" type="array" required>
  Array of logistics request objects. Each record uses the same schema as the single [Create Logistics Request](/api-reference/logistics/create-request) endpoint.

  **Maximum items:** 5,000
</ParamField>

<Expandable title="Record Fields">
  <ParamField body="origin" type="string">
    Origin location (address or hub name). Required if `distance_km` not provided.
  </ParamField>

  <ParamField body="destination" type="string">
    Destination location (address or hub name). Required if `distance_km` not provided.
  </ParamField>

  <ParamField body="distance_km" type="number">
    Distance in kilometers. If provided, takes priority over origin/destination.
  </ParamField>

  <ParamField body="load" type="number">
    Load of the shipment. If not provided, uses TOC default.
  </ParamField>

  <ParamField body="load_unit" type="string">
    Unit of the load: `kg`, `ton`, `pallets`, `teu`, `feu`. Default: `kg`
  </ParamField>

  <ParamField body="load_factor" type="number">
    Factor to multiply the load by (0-1). Default: `1.0`
  </ParamField>

  <ParamField body="toc" type="string">
    Type of vehicle (e.g., `van_diesel`). If omitted, estimated from category.
  </ParamField>

  <ParamField body="category" type="string" required>
    Vehicle category: `road`, `rail`, `maritime`, `air`. Required if `toc` not provided.
  </ParamField>

  <ParamField body="origin_country" type="string">
    ISO 3166-1 alpha-2 country code for regional emission factors. Default: `GLO`
  </ParamField>

  <ParamField body="year" type="integer">
    Year for emission factors. Default: current year.
  </ParamField>

  <ParamField body="cleaning" type="boolean">
    Whether cleaning was done (increases emissions). Default: `false`
  </ParamField>

  <ParamField body="movement_id" type="string">
    Unique shipment identifier for tracking.
  </ParamField>

  <ParamField body="client" type="string">
    Client or customer identifier.
  </ParamField>

  <ParamField body="shipment_date" type="date">
    Date of shipment (ISO 8601). Default: current date.
  </ParamField>

  <ParamField body="movement_stretch" type="string">
    Stretch of the movement (e.g., `first_mile`, `last_mile`).
  </ParamField>

  <ParamField body="movement_stage" type="string">
    Stage in the logistics chain (e.g., `pickup`, `delivery`).
  </ParamField>

  <ParamField body="vehicle_license_plate" type="string">
    Vehicle license plate.
  </ParamField>

  <ParamField body="trailer_license_plate" type="string">
    Trailer license plate.
  </ParamField>

  <ParamField body="subcontractor" type="boolean">
    Whether handled by a subcontractor. Default: `true`
  </ParamField>

  <ParamField body="tkm" type="number">
    Tonne-kilometers (if already calculated).
  </ParamField>

  <ParamField body="package_key" type="string">
    Unique package identifier for multi-leg tracking. Requires `movement_id`.
  </ParamField>
</Expandable>

<ParamField body="options" type="object">
  Processing options for the bulk operation.
</ParamField>

<Expandable title="Options Fields">
  <ParamField body="continue_on_error" type="boolean">
    If `true` (default), continues processing remaining records when one fails.
    If `false`, stops at the first error.

    **Default:** `true`
  </ParamField>
</Expandable>

## Response

<ResponseField name="total_received" type="integer">
  Total number of records received in the request
</ResponseField>

<ResponseField name="total_processed" type="integer">
  Total records processed (success + failed)
</ResponseField>

<ResponseField name="success" type="integer">
  Number of records processed successfully
</ResponseField>

<ResponseField name="failed" type="integer">
  Number of records that failed
</ResponseField>

<ResponseField name="results" type="array">
  Array of successfully processed records with essential fields
</ResponseField>

<Expandable title="Result Item Fields">
  <ResponseField name="index" type="integer">
    Index of the record in the original array (0-based)
  </ResponseField>

  <ResponseField name="id" type="string">
    UUID of the created logistics request
  </ResponseField>

  <ResponseField name="movement_id" type="string">
    Movement ID from the input record (if provided)
  </ResponseField>

  <ResponseField name="package_id" type="string">
    UUID of the associated package (if `package_key` was provided)
  </ResponseField>

  <ResponseField name="co2e" type="number">
    Calculated CO2e emissions in kg
  </ResponseField>
</Expandable>

<ResponseField name="errors" type="array">
  Array of errors for failed records
</ResponseField>

<Expandable title="Error Item Fields">
  <ResponseField name="index" type="integer">
    Index of the failed record in the original array (0-based)
  </ResponseField>

  <ResponseField name="movement_id" type="string">
    Movement ID from the input record (if present)
  </ResponseField>

  <ResponseField name="error" type="string">
    Error description
  </ResponseField>
</Expandable>

## Example

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST "https://api.dcycle.io/v1/logistics/requests/bulk" \
    -H "x-api-key: ${DCYCLE_API_KEY}" \
    -H "x-organization-id: ${DCYCLE_ORG_ID}" \
    -H "Content-Type: application/json" \
    -d '{
      "records": [
        {
          "origin": "Madrid, Spain",
          "destination": "Barcelona, Spain",
          "load": 500,
          "load_unit": "kg",
          "category": "road",
          "movement_id": "SHIP-001",
          "client": "AMAZON"
        },
        {
          "origin": "Madrid, Spain",
          "destination": "Barcelona, Spain",
          "load": 300,
          "load_unit": "kg",
          "category": "road",
          "movement_id": "SHIP-002",
          "client": "AMAZON"
        },
        {
          "origin": "Valencia, Spain",
          "destination": "Sevilla, Spain",
          "load": 1000,
          "load_unit": "kg",
          "category": "road",
          "movement_id": "SHIP-003",
          "client": "ZARA"
        }
      ],
      "options": {
        "continue_on_error": true
      }
    }'
  ```

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

  api_key = os.getenv("DCYCLE_API_KEY")
  org_id = os.getenv("DCYCLE_ORG_ID")

  headers = {
      "x-api-key": api_key,
      "x-organization-id": org_id,
      "Content-Type": "application/json"
  }

  payload = {
      "records": [
          {
              "origin": "Madrid, Spain",
              "destination": "Barcelona, Spain",
              "load": 500,
              "load_unit": "kg",
              "category": "road",
              "movement_id": "SHIP-001",
              "client": "AMAZON"
          },
          {
              "origin": "Madrid, Spain",
              "destination": "Barcelona, Spain",
              "load": 300,
              "load_unit": "kg",
              "category": "road",
              "movement_id": "SHIP-002",
              "client": "AMAZON"
          },
          {
              "origin": "Valencia, Spain",
              "destination": "Sevilla, Spain",
              "load": 1000,
              "load_unit": "kg",
              "category": "road",
              "movement_id": "SHIP-003",
              "client": "ZARA"
          }
      ],
      "options": {
          "continue_on_error": True
      }
  }

  response = requests.post(
      "https://api.dcycle.io/v1/logistics/requests/bulk",
      headers=headers,
      json=payload
  )

  result = response.json()
  print(f"Processed: {result['success']}/{result['total_received']}")
  print(f"Failed: {result['failed']}")
  print(f"Total CO2e: {sum(r['co2e'] for r in result['results']):.2f} kg")
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const axios = require('axios');

  const apiKey = process.env.DCYCLE_API_KEY;
  const orgId = process.env.DCYCLE_ORG_ID;

  const headers = {
    'x-api-key': apiKey,
    'x-organization-id': orgId,
    'Content-Type': 'application/json'
  };

  const payload = {
    records: [
      {
        origin: "Madrid, Spain",
        destination: "Barcelona, Spain",
        load: 500,
        load_unit: "kg",
        category: "road",
        movement_id: "SHIP-001",
        client: "AMAZON"
      },
      {
        origin: "Madrid, Spain",
        destination: "Barcelona, Spain",
        load: 300,
        load_unit: "kg",
        category: "road",
        movement_id: "SHIP-002",
        client: "AMAZON"
      },
      {
        origin: "Valencia, Spain",
        destination: "Sevilla, Spain",
        load: 1000,
        load_unit: "kg",
        category: "road",
        movement_id: "SHIP-003",
        client: "ZARA"
      }
    ],
    options: {
      continue_on_error: true
    }
  };

  axios.post(
    'https://api.dcycle.io/v1/logistics/requests/bulk',
    payload,
    { headers }
  )
  .then(response => {
    const result = response.data;
    console.log(`Processed: ${result.success}/${result.total_received}`);
    console.log(`Failed: ${result.failed}`);
    const totalCo2e = result.results.reduce((sum, r) => sum + r.co2e, 0);
    console.log(`Total CO2e: ${totalCo2e.toFixed(2)} kg`);
  })
  .catch(error => console.error(error));
  ```
</CodeGroup>

### Successful Response

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "total_received": 3,
  "total_processed": 3,
  "success": 3,
  "failed": 0,
  "results": [
    {
      "index": 0,
      "id": "0d9b8817-fc47-47c3-b134-b3565ae1a57f",
      "movement_id": "SHIP-001",
      "package_id": null,
      "co2e": 20.99
    },
    {
      "index": 1,
      "id": "683a0184-6dac-44ab-80a2-ce31b14ce927",
      "movement_id": "SHIP-002",
      "package_id": null,
      "co2e": 12.59
    },
    {
      "index": 2,
      "id": "1048a12d-46e6-4e0d-955e-833d4693bcab",
      "movement_id": "SHIP-003",
      "package_id": null,
      "co2e": 43.99
    }
  ],
  "errors": []
}
```

### Response with Partial Errors

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "total_received": 3,
  "total_processed": 3,
  "success": 2,
  "failed": 1,
  "results": [
    {
      "index": 0,
      "id": "0d9b8817-fc47-47c3-b134-b3565ae1a57f",
      "movement_id": "SHIP-001",
      "package_id": null,
      "co2e": 20.99
    },
    {
      "index": 2,
      "id": "1048a12d-46e6-4e0d-955e-833d4693bcab",
      "movement_id": "SHIP-003",
      "package_id": null,
      "co2e": 43.99
    }
  ],
  "errors": [
    {
      "index": 1,
      "movement_id": "SHIP-002",
      "error": "TOC not found for toc=invalid_toc, category=None, region=EU"
    }
  ]
}
```

## Performance Optimizations

The bulk endpoint includes several optimizations for high-volume processing:

<AccordionGroup>
  <Accordion title="TOC Caching">
    All active vehicle types (TOCs) are pre-loaded at the start of the request, avoiding repeated database queries for each record.
  </Accordion>

  <Accordion title="Hub Caching">
    Organization logistics hubs are pre-loaded, allowing instant name-to-address resolution without individual lookups.
  </Accordion>

  <Accordion title="Distance Caching">
    Calculated distances are cached by route (origin, destination, category). Repeated routes reuse cached distances, reducing Google Maps API calls.

    **Example:** If 100 records share the same Madrid → Barcelona route, the distance is calculated only once.
  </Accordion>

  <Accordion title="Batch Database Operations">
    Records are inserted in batches of 500 for efficient database writes.
  </Accordion>
</AccordionGroup>

## Use Cases

### Daily Batch Upload (Logistics Provider)

Process daily shipment data from a logistics provider:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import pandas as pd

def upload_daily_shipments(csv_file: str):
    """Upload daily shipments from CSV file"""
    df = pd.read_csv(csv_file)

    records = []
    for _, row in df.iterrows():
        records.append({
            "origin": row["origin"],
            "destination": row["destination"],
            "load": row["weight_kg"],
            "load_unit": "kg",
            "category": "road",
            "movement_id": row["tracking_number"],
            "client": row["client_code"],
            "shipment_date": row["date"],
            "distance_km": row.get("distance_km"),  # Use if available
        })

    # Process in chunks of 5000
    chunk_size = 5000
    total_success = 0
    total_failed = 0

    for i in range(0, len(records), chunk_size):
        chunk = records[i:i + chunk_size]

        response = requests.post(
            "https://api.dcycle.io/v1/logistics/requests/bulk",
            headers=headers,
            json={"records": chunk, "options": {"continue_on_error": True}}
        )

        result = response.json()
        total_success += result["success"]
        total_failed += result["failed"]

        if result["errors"]:
            print(f"Chunk {i//chunk_size + 1} errors: {result['errors']}")

    print(f"Total: {total_success} success, {total_failed} failed")
    return total_success, total_failed

# Example: Upload 400k daily records
upload_daily_shipments("daily_shipments_2024-01-15.csv")
```

### Multi-leg Package Tracking (Bulk)

Track packages through multiple legs in a single request:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def track_packages_bulk(packages_data: list):
    """
    Track multiple packages with their legs in bulk.

    packages_data format:
    [
        {
            "package_key": "PKG-001",
            "movement_id": "SHIP-001",
            "legs": [
                {"origin": "Madrid Hub", "destination": "Valencia Hub", "load_kg": 15},
                {"origin": "Valencia Hub", "destination": "Barcelona Hub", "load_kg": 15},
            ]
        },
        ...
    ]
    """
    records = []

    for pkg in packages_data:
        for leg in pkg["legs"]:
            records.append({
                "origin": leg["origin"],
                "destination": leg["destination"],
                "load": leg["load_kg"],
                "load_unit": "kg",
                "category": "road",
                "package_key": pkg["package_key"],
                "movement_id": pkg["movement_id"],
            })

    response = requests.post(
        "https://api.dcycle.io/v1/logistics/requests/bulk",
        headers=headers,
        json={"records": records, "options": {"continue_on_error": True}}
    )

    return response.json()
```

### With Pre-calculated Distances

When you have pre-calculated distances (e.g., from your routing system), skip geocoding:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
records = [
    {
        "distance_km": 620,  # Pre-calculated
        "load": 500,
        "load_unit": "kg",
        "category": "road",
        "origin_country": "ES",
        "movement_id": "SHIP-001",
    },
    {
        "distance_km": 450,
        "load": 300,
        "load_unit": "kg",
        "category": "road",
        "origin_country": "ES",
        "movement_id": "SHIP-002",
    },
]

response = requests.post(
    "https://api.dcycle.io/v1/logistics/requests/bulk",
    headers=headers,
    json={"records": records}
)
```

## Common Errors

### 401 Unauthorized

**Cause:** Missing or invalid API key

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"detail": "Invalid API key for organization", "code": "INVALID_API_KEY"}
```

### 403 Forbidden

**Cause:** The authenticated user is not a member of the organization

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"detail": "Logged User is not Member of Organization", "code": "LOGGED_USER_NOT_MEMBER"}
```

### 422 Validation Error

**Cause:** Invalid record format or missing required fields

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "detail": [
    {
      "loc": ["body", "records", 0, "category"],
      "msg": "Please provide a category if toc is not present",
      "type": "value_error"
    }
  ]
}
```

**Solution:** Ensure each record has either `toc` or `category`, and either `distance_km` or both `origin` and `destination`.

### Partial Failures

When some records fail but `continue_on_error` is `true`, the response includes both successful results and errors:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "success": 98,
  "failed": 2,
  "results": [...],
  "errors": [
    {"index": 5, "movement_id": "SHIP-006", "error": "TOC not found"},
    {"index": 42, "movement_id": "SHIP-043", "error": "Load not found and no default available"}
  ]
}
```

**Solution:** Review the `errors` array to identify and fix problematic records, then resubmit only the failed ones.

## Limits

| Limit                       | Value                   |
| --------------------------- | ----------------------- |
| Maximum records per request | 5,000                   |
| Maximum concurrent requests | 6 per API key           |
| Request timeout             | 5 minutes               |
| Recommended batch size      | 1,000 - 2,000           |
| Recommended concurrency     | 5 (one below the limit) |

When the concurrency limit is exceeded, the API returns HTTP `429` with a `Retry-After` header.

<Tip>
  For very large uploads (e.g. 1.5M records), split into batches of 2,000 records and send up to 5 requests in parallel. Wait for one to finish before sending the next. On a `429` response, wait the number of seconds indicated in the `Retry-After` header before retrying.
</Tip>

## Related Endpoints

<CardGroup cols={2}>
  <Card title="Create Single Request" icon="plus" href="/api-reference/logistics/create-request">
    Create a single logistics request
  </Card>

  <Card title="Get Requests" icon="list" href="/api-reference/logistics/get-requests">
    Retrieve all logistics requests with pagination
  </Card>

  <Card title="Get Packages" icon="box" href="/api-reference/logistics/get-packages">
    Retrieve packages with aggregated emissions
  </Card>

  <Card title="Get TOCs" icon="truck" href="/api-reference/logistics/get-tocs">
    Get available vehicle types
  </Card>
</CardGroup>
