Skip to main content
POST
Create Logistics Requests (Bulk, v2)
← 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 (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-Key is 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.
Changed on 2026-10-13. This endpoint used to answer 200 with results[]/errors[] (the v1 shape), keep the valid records of a partly invalid batch, and accept options.continue_on_error. See the changelog.

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:
Legs are upserted on their business key (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, with Location: /v2/ingest-jobs/{id}.
With dry_run=true and every record valid: 200 with {"dry_run": true, "submitted": 5000, "valid": 5000}.

Job status

Poll GET /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’s error_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). Other codes from the distance engine may appear; they are stable and come with a 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.

422 IDEMPOTENCY_KEY_REUSED

The Idempotency-Key was already used with a different body. Use a new key for a new batch.

422 REQUEST_VALIDATION_FAILED — malformed request

A missing Idempotency-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 of x-organization-id.

Examples

Create a batch and wait for the calculation

Python

Retries and idempotency

Retry a timed-out or failed request with the same Idempotency-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

Requests over the cap get HTTP 429 with a Retry-After header. The cap is on requests in flight at once, not on requests per second — see Rate Limits.

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