Routing API
The Routing API optimizes orders against a fleet of Curri vehicles. Use it when you already know the shipments and vehicles you want to evaluate and need Curri to return the best stop order, or when you want Curri to create/update itineraries from that optimized plan.
The OpenAPI source lives in apps/curri-api/openapi/v1-routing.yaml. The
rendered REST reference is available at
/routing-api-reference.
Summary
- Use
POST /v1/routing/runorPOST /v1/routing/planto preview optimized tours without writing itineraries. - Use
POST /v1/routing/bookto optimize and create/update Curri itineraries. - Use
problemIdas the idempotency key and public job ID. - If a plan request takes longer than the public sync window, poll
GET /v1/routing/jobs/:problemId.
Routing Endpoints
| Endpoint | Mode | Writes to Curri | Notes |
|---|---|---|---|
POST /v1/routing/run | plan only | No | Starts a plan run and may return 202 while processing. |
POST /v1/routing/plan | forced to plan | No | Alias for plan-only callers. |
POST /v1/routing/book | createItinerary, updateItinerary | Yes | Runs optimization and writes itineraries synchronously. |
GET /v1/routing/jobs/:id | N/A | No | Polls a plan job by public problemId. |
Authentication
Routing endpoints accept either Curri JWT auth or API-key Basic auth. For API-key auth, send:
Authorization: Basic <base64(externalUserId:apiKey)>
The server attempts JWT auth first, then API-key Basic auth.
Idempotency
Set problemId to a stable, caller-generated value. It doubles as the public
job ID you poll. Idempotency is scoped per account: internally the queue key is
namespaced as {accountId}:{problemId}, so two different accounts may safely
reuse the same problemId. Because requests are idempotent, retrying a 500
with the same problemId is safe.
Required Input Parameters
| Parameter | Type | Description |
|---|---|---|
mode | String | Required on /run and /book. Use plan for /run; use createItinerary or updateItinerary for /book. /plan forces this value to plan. |
problemId | String | Caller-supplied idempotency key. This also becomes the public job ID for polling. |
orders | Array | Shipment payloads to assign to the optimized tours. Each order should include a stable externalId so Curri can return unassigned reasons. |
Optional Input Parameters
| Parameter | Type | Description |
|---|---|---|
accountId | Number | Numeric internal account ID. Omit this to use the authenticated user's default account. String account IDs are rejected on the public REST surface. |
fleet | Array | Fleet entries to optimize against. Use FleetSpec objects for new tours, or full itinerary-shaped objects for power/update flows. Required by the routing service for plan and create modes. |
targetItineraryIds | Array | Existing itinerary IDs to update when using mode: "updateItinerary". |
options | Object | Per-request routing options. These override account-effective settings and Curri defaults. |
Order Fields
Each entry in orders describes one shipment. Provide a stable externalId so
Curri can map unassigned reasons back to your order. Addresses and time windows
are not enforced by the request validator, but the optimizer needs pickup and
dropoff addresses to produce a usable route.
| Field | Type | Description |
|---|---|---|
externalId | String | Stable shipment ID. Used to key unassigned reasons. |
pickupAddress | Address | Where the shipment is picked up. |
dropoffAddress | Address | Where the shipment is dropped off. |
pickupWindowOpen | ISO 8601 date-time | Earliest pickup time. |
pickupWindowClose | ISO 8601 date-time | Latest pickup time. |
dropoffWindowOpen | ISO 8601 date-time | Earliest dropoff time. |
dropoffWindowClose | ISO 8601 date-time | Latest dropoff time. |
payloadItems | Array | Items being moved (see Payload Items). |
customTags | Array | Tags used for tag-based prioritization, sent as { "name": "lift-gate" } objects. |
Address Fields
pickupAddress and dropoffAddress share the same shape. Send latitude and
longitude (as strings) to skip geocoding.
| Field | Type | Description |
|---|---|---|
addressLine1 | String | Street address. |
city | String | City. |
state | String | State or region. |
postalCode | String | Postal code. |
latitude | String | Optional. Decimal degrees, sent as a string. |
longitude | String | Optional. Decimal degrees, sent as a string. |
Payload Items
Each entry in an order's payloadItems describes one item being moved.
| Field | Type | Description |
|---|---|---|
description | String | Human-readable item description. |
height | Number | Height in inches. |
length | Number | Length in inches. |
width | Number | Width in inches. |
weight | Number | Weight in pounds. |
quantity | Number | Number of identical items. |
id | String | Optional caller-supplied item ID. |
Routing payload dimensions are in inches and weights are in pounds. This differs from the GraphQL deliveries/itineraries API, which uses centimeters and kilograms.
Fleet Fields
Each entry in fleet describes a driver/vehicle pair to optimize against. Power
and update flows may instead pass full itinerary-shaped objects through fleet.
| Field | Type | Required | Description |
|---|---|---|---|
driverExternalId | String | Yes | Stable driver ID. |
vehicleExternalId | String | Yes | Stable vehicle ID. |
vehicleType | String | Yes | Vehicle type, e.g. cargo-van or box-truck. |
scheduledStart | ISO 8601 date-time | Yes | When the vehicle starts its route. |
vehicleSpec | Object | No | Per-vehicle capacity and cost overrides. |
routeSkills | [String] | No | Skills this vehicle provides for skill-matched orders. |
externalId | String | No | Optional fleet-entry ID. |
meta | Object | No | Arbitrary caller metadata. |
Mode Behavior
| Mode | Description |
|---|---|
plan | Returns an optimized route without writing to Curri's database. Use for previews, feasibility checks, and customer-facing planning flows. |
createItinerary | Optimizes and creates new Curri itineraries. Use when the optimized tours should become dispatchable. |
updateItinerary | Optimizes and updates existing Curri itineraries. Use when reflowing routes that already exist in Curri. |
Response Fields
Successful 200 responses return a routing result.
| Field | Type | Description |
|---|---|---|
problemId | String | The idempotency key for this optimization request. |
itineraries | Array | Optimized itinerary objects. In plan mode these may be unpersisted planned itineraries; in book modes these are persisted itinerary-shaped objects. |
unassigned | Array | Orders the optimizer could not place, keyed by externalId, with optional rejection reasons from HERE. |
When an order cannot be placed, it appears in unassigned with one or more
reason codes:
{
"unassigned": [
{
"externalId": "order-002",
"reasons": [
{
"code": "CAPACITY_CONSTRAINT",
"description": "No vehicle has enough cargo capacity."
}
]
}
]
}
When /run or /plan is still processing, Curri returns 202:
{
"jobId": "routing-plan-2026-06-18-001",
"problemId": "routing-plan-2026-06-18-001",
"status": "processing",
"statusUrl": "/v1/routing/jobs/routing-plan-2026-06-18-001"
}
Pending poll responses from GET /v1/routing/jobs/:id are smaller:
{ "status": "processing" }
Polling and Timeouts
POST /v1/routing/runandPOST /v1/routing/planwait up to the sync timeout (30 seconds by default, capped at 120 seconds) for a result. If the optimizer finishes in time you get200; otherwise you get202with astatusUrl.POST /v1/routing/bookis fully synchronous and never returns202.- Poll
GET /v1/routing/jobs/:problemIduntil it returns200. While the job runs it returns202with{ "status": "processing" }. - Completed jobs are retained for roughly 30 minutes. Polling after that returns
404with codejob_not_found. - There are no routing webhooks today; use polling to detect completion.
Options Precedence
Routing options are merged in this order:
- Curri's default optimization options.
- The account's effective optimization settings, including inherited parent account settings.
- Per-request
options.
Per-request options win. Map-like options (lockedStops, tagPriorities,
vehicleMaps, vehicleSpecOverrides) must be sent as arrays of [key, value]
tuples across JSON boundaries, not as JavaScript Map objects.
Example:
{
"options": {
"objective": "MIN_VEHICLE_UTILIZATION",
"trafficMode": "none",
"tagPriorities": [["lift-gate", 5]]
}
}
Common Options
These are the options callers set most often. See the full REST reference for the complete list.
| Option | Type | Allowed values / notes |
|---|---|---|
objective | String | One of MIN_DURATION, MIN_MAX_TRAVEL_COST_DISTANCE, MIN_VEHICLE_AVOID_TOUR_OVERLAP, MIN_VEHICLE_UTILIZATION, or a provider-specific objective string. |
trafficMode | String | automatic or none. |
enableStopClustering | Boolean | Group nearby stops into a single visit. |
volumeOptimization | Boolean | Factor payload volume into vehicle packing. |
bundlePayloadItems | Boolean | Keep an order's payload items together. |
tagPriorities | Array | [tag, priority] tuples; priority ranges from 0 to 5. |
lockedStops | Array | [itineraryExternalId, [stops]] tuples to pin stop order. |
vehicleMaps, vehicleSpecOverrides | Array | [vehicleTypeOrExternalId, mapping] tuples for per-vehicle overrides. |
Examples
Plan a Route
Endpoint:
POST /v1/routing/run
Example request body:
{
"mode": "plan",
"problemId": "routing-plan-2026-06-18-001",
"fleet": [
{
"driverExternalId": "driver-123",
"vehicleExternalId": "vehicle-456",
"vehicleType": "cargo-van",
"scheduledStart": "2026-06-18T15:00:00.000Z"
}
],
"orders": [
{
"externalId": "order-001",
"pickupAddress": {
"addressLine1": "100 Pickup Way",
"city": "San Francisco",
"state": "CA",
"postalCode": "94103",
"latitude": "37.7749",
"longitude": "-122.4194"
},
"dropoffAddress": {
"addressLine1": "200 Dropoff Ave",
"city": "Oakland",
"state": "CA",
"postalCode": "94607",
"latitude": "37.8044",
"longitude": "-122.2712"
},
"pickupWindowOpen": "2026-06-18T15:00:00.000Z",
"pickupWindowClose": "2026-06-18T17:00:00.000Z",
"dropoffWindowOpen": "2026-06-18T18:00:00.000Z",
"dropoffWindowClose": "2026-06-18T20:00:00.000Z"
}
]
}
Example 200 response:
{
"problemId": "routing-plan-2026-06-18-001",
"itineraries": [
{
"fleetIndex": 0,
"itinerary": {
"externalId": "fleet_driver-123_vehicle-456",
"scheduledStart": "2026-06-18T15:00:00.000Z",
"itineraryStops": [
{
"stopNumber": 1,
"address": {
"addressLine1": "100 Pickup Way",
"city": "San Francisco",
"state": "CA",
"postalCode": "94103"
}
},
{
"stopNumber": 2,
"address": {
"addressLine1": "200 Dropoff Ave",
"city": "Oakland",
"state": "CA",
"postalCode": "94607"
}
}
]
}
}
],
"unassigned": []
}
Book Optimized Itineraries
Endpoint:
POST /v1/routing/book
Example request body:
{
"mode": "createItinerary",
"problemId": "routing-book-2026-06-18-001",
"fleet": [
{
"driverExternalId": "driver-123",
"vehicleExternalId": "vehicle-456",
"vehicleType": "cargo-van",
"scheduledStart": "2026-06-18T15:00:00.000Z"
}
],
"orders": [
{
"externalId": "order-001",
"pickupAddress": {
"addressLine1": "100 Pickup Way",
"city": "San Francisco",
"state": "CA",
"postalCode": "94103",
"latitude": "37.7749",
"longitude": "-122.4194"
},
"dropoffAddress": {
"addressLine1": "200 Dropoff Ave",
"city": "Oakland",
"state": "CA",
"postalCode": "94607",
"latitude": "37.8044",
"longitude": "-122.2712"
},
"pickupWindowOpen": "2026-06-18T15:00:00.000Z",
"pickupWindowClose": "2026-06-18T17:00:00.000Z",
"dropoffWindowOpen": "2026-06-18T18:00:00.000Z",
"dropoffWindowClose": "2026-06-18T20:00:00.000Z"
}
]
}
Poll a Routing Job
Endpoint:
GET /v1/routing/jobs/routing-plan-2026-06-18-001
Example processing response:
{
"status": "processing"
}
When processing completes, the endpoint returns the same routing result shape shown in the plan example.
Error Model
Errors always use a nested error object. type is the broad category
(invalid_request_error, authentication_error, or api_error), code is the
specific reason, message is human-readable, and param names the offending
field when applicable.
{
"error": {
"type": "invalid_request_error",
"code": "invalid_routing_input",
"message": "routing.plan: input.fleet is required for plan and createItinerary modes",
"param": "fleet"
}
}
Common statuses:
| Status | Meaning |
|---|---|
400 | Request shape or routing input validation failed. |
401 | Authentication failed, or the authenticated user has no default account. |
403 | The request accountId does not match the authenticated account. |
404 | Job or routing endpoint not found. |
500 | Queue, optimization, or booking failed. Retry is safe with the same problemId. |
Error Codes
| Code | Status | Meaning |
|---|---|---|
unauthenticated | 401 | No valid JWT or API-key credentials were supplied. |
no_account_for_user | 401 | The authenticated user has no default account. |
invalid_input | 400 | The request body failed shape validation. |
invalid_routing_input | 400 | Routing-specific validation failed (for example, missing fleet). |
string_account_id_unsupported | 400 | A string accountId was sent; send the numeric ID or omit it. |
invalid_job_id | 400 | The job id path parameter was missing or empty. |
account_mismatch | 403 | The request accountId does not match the credential's account. |
not_found | 404 | Unknown routing endpoint. |
job_not_found | 404 | No job with that id, or it expired after ~30 minutes. |
enqueue_failed | 500 | The routing job could not be enqueued. |
optimization_failed | 500 | The optimizer did not return a usable result. |
unknown_job_state | 500 | The job ended in an unexpected state. |
book_failed | 500 | Booking or upsert failed after optimization. |
job_lookup_failed | 500 | Reading the job store failed while polling. |
The routing surface reserves 429 (rate_limit_error) for future rate limiting
but does not emit it today. It does not emit 409.