> ## 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 Logistic Hubs (v2)

> Create up to 1,000 logistic hubs in one request and get them back

[← Logistic Hubs API](/api-reference/logistic-hubs/overview)

Create one or many logistic hubs: your warehouses, cross-docks and transshipment points, and the third-party ones your
shipments go through. A hub is master data: creating it calculates nothing (an owned hub's emissions are those of its
[facility](/api-reference/facilities/list)), so the request is **synchronous**: it answers `201 Created` with every
hub, in the order you sent them. Create your hubs before the logistics requests that reference them.

> [!WARNING]
>
> **Beta.** Part of the bulk ingest API, currently 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/logistic-hubs" \
  -H "x-api-key: ${DCYCLE_API_KEY}" \
  -H "x-organization-id: ${DCYCLE_ORG_ID}" \
  -H "Idempotency-Key: 5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a" \
  -H "Content-Type: application/json" \
  -d '{
    "records": [
      {
        "name": "MAD-01",
        "type": "owned",
        "country": "ES",
        "address": "Calle de la Logística 12, 28042 Madrid",
        "category": "warehouse_ambient",
        "facility_id": "c2a8f6b4-3e1d-4f7a-9b2c-5d6e7f8a9b0c",
        "client_row_id": "HUB-001"
      },
      {
        "name": "PART-BCN",
        "type": "subcontracted",
        "country": "ES",
        "address": "Carrer del Port 3, 08039 Barcelona"
      }
    ]
  }'
```

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

import requests

response = requests.post(
    "https://api.dcycle.io/v2/logistic-hubs",
    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": [
            {
                "name": "PART-BCN",
                "type": "subcontracted",
                "country": "ES",
                "address": "Carrer del Port 3, 08039 Barcelona",
            }
        ]
    },
    timeout=60,
)
response.raise_for_status()
for item in response.json()["items"]:
    print(item["hub"]["name"], item["hub"]["id"])
```

```json 201 theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "items": [
    {
      "source_row_index": 0,
      "client_row_id": "HUB-001",
      "hub": {
        "id": "4f3e2d1c-0b9a-4876-9543-210fedcba987",
        "organization_id": "a8315ef3-dd50-43f8-b7ce-d839e68d51fa",
        "name": "MAD-01",
        "type": "owned",
        "country": "ES",
        "address": "Calle de la Logística 12, 28042 Madrid",
        "category": "warehouse_ambient",
        "facility_id": "c2a8f6b4-3e1d-4f7a-9b2c-5d6e7f8a9b0c",
        "supercharger": false,
        "status": "active",
        "created_at": "2026-10-20T09:12:44.031Z",
        "updated_at": "2026-10-20T09:12:44.031Z"
      }
    }
  ]
}
```

```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": "HUB-001",
      "error_code": "HUB_NAME_ALREADY_EXISTS",
      "error_params": { "field": "name", "value": "MAD-01" }
    }
  ],
  "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:** the name is free, the country and the category exist, and an owned hub's facility is
   usable and not already linked to another hub. If any record is invalid the **whole** request is rejected with `422` and nothing is created; the
   answer lists every invalid record.
2. **Every hub is created in one transaction** and returned, in the order sent. `source_row_index` is the position of
   the record in `records`, and `client_row_id` echoes yours.
3. **The address is stored as you send it; it is not geocoded here.** Logistics requests that reference the hub take
   it as their origin and geocode it when they are calculated: an address that cannot be located shows up then, as a
   per-leg error of those requests. Send the full address (street, postal code, city).
4. **The hubs belong to the `x-organization-id` organization.** A record cannot name another one: send each
   organization's hubs with its own `x-organization-id`.
5. **Logistics requests find a hub by its exact `name`** (the `hub_id` of [Create Logistics Requests
   (v2)](/api-reference/logistics/create-requests-v2)): same case, no extra spaces. Use the code your own systems use
   for it.

## Request

### Headers

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

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

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

The organization the hubs belong to.

**Idempotency-Key** `string` *required*

1 to 255 characters, unique per batch. A retry with the same key and body answers the same hubs (with
`Idempotent-Replayed: true`) instead of creating them 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`.

### Query Parameters

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

Check only: the same `422` for invalid records, nothing created; `200` with `{"dry_run": true, "submitted": n,
    "valid": n}` when every record is valid.

### Body Parameters

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

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

**record**

**name** `string` *required*

1 to 255 characters, stored trimmed: the hub's code, as your logistics requests send it in `hub_id`. It must be
free in the organization (archived hubs included) and appear once in the request.

**type** `string` *required*

`owned` (a hub your organization runs: give its `facility_id`) or `subcontracted` (run by a third party).

**country** `string` *required*

Where the hub is, ISO 3166-1 alpha-2 (`ES`, `FR`...; case-insensitive, returned uppercase).

**address** `string` *required*

1 to 500 characters, stored trimmed and not geocoded. Logistics requests that reference the hub use it as their
origin.

**category** `string` (default: transshipment\_ambient)

The hub operation: a `category` of [List HOCs](/api-reference/logistics/list-hocs) (`transshipment_ambient`,
`warehouse_mixed`, `storage_transhipment_ambient`...).

**facility\_id** `uuid`

Required for `owned`, not allowed for `subcontracted`: the [facility](/api-reference/facilities/list) the hub is,
of your organization or of a subsidiary you can write to, enabled and not archived. One hub per facility: a
facility already linked to another hub, or sent twice, is rejected. An owned hub's emissions are its
facility's.

**supercharger** `boolean` (default: false)

Whether the hub is an electric charging point.

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

Your own reference for the record, up to 255 characters, echoed in the answer and in errors.

## Response

`201 Created` with `items`: one per record, in the order sent, each with `source_row_index`, `client_row_id` and the
created `hub`: `id`, `organization_id`, `name`, `type`, `country`, `address`, `category`, `facility_id`,
`supercharger`, `status` (`active`) and UTC timestamps ending in `Z`.

## Errors

[Problem details](/api-reference/errors). Per-record codes in `errors[].error_code`:

| `error_code` | Cause |
| - | - |
| `SCHEMA_INVALID` | The record does not match the schema: a missing or unknown field (e.g. `organization_id`), a `type` other than `owned`/`subcontracted`, an empty address, a country that is not two letters... `error_params.errors` says where. |
| `HUB_NAME_ALREADY_EXISTS` | A hub of the organization (archived ones included) already has this name. |
| `DUPLICATE_HUB_NAME` | Another record of the request has the same name. |
| `REFERENCE_NOT_FOUND` | `country` or `category` does not exist, or `facility_id` does not exist, is disabled or archived, or is outside your organization and the subsidiaries you can write to (all answer the same; `error_params.field` says which field). |
| `FACILITY_ALREADY_LINKED_TO_HUB` | `facility_id` is already the facility of another hub, or appears in two records of the request. |
| `FACILITY_REQUIRED_FOR_OWNED_HUB` | An `owned` hub without `facility_id`. |
| `FACILITY_NOT_ALLOWED_FOR_SUBCONTRACTED_HUB` | A `subcontracted` hub with `facility_id`. |

`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 1,000, no
`Idempotency-Key`), `422 IDEMPOTENCY_KEY_REUSED`, `409 IDEMPOTENCY_KEY_IN_PROGRESS`, and `429` beyond the
[rate limits](/api-reference/rate-limits).

## Retries and idempotency

* **Network error or timeout:** retry with the **same** `Idempotency-Key` and body. If the first request was
  accepted you get the same hubs back, with `Idempotent-Replayed: true`, and nothing is duplicated.
* **`422 BULK_RECORDS_REJECTED`:** nothing was created. Fix the records and send the batch again.

## Limits

| Limit | Value |
| - | - |
| Records per request | 1 to 1,000 |

## Related Endpoints

> **[List Logistic Hubs](/api-reference/logistic-hubs/list)**
>
> The organization's hubs and their ids

> **[List HOCs](/api-reference/logistics/list-hocs)**
>
> Values for `category`

> **[List Facilities](/api-reference/facilities/list)**
>
> Ids for `facility_id`

> **[Create Logistics Requests (v2)](/api-reference/logistics/create-requests-v2)**
>
> The shipment legs that reference your hubs


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