Skip to main content

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/run or POST /v1/routing/plan to preview optimized tours without writing itineraries.
  • Use POST /v1/routing/book to optimize and create/update Curri itineraries.
  • Use problemId as 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

EndpointModeWrites to CurriNotes
POST /v1/routing/runplan onlyNoStarts a plan run and may return 202 while processing.
POST /v1/routing/planforced to planNoAlias for plan-only callers.
POST /v1/routing/bookcreateItinerary, updateItineraryYesRuns optimization and writes itineraries synchronously.
GET /v1/routing/jobs/:idN/ANoPolls 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

ParameterTypeDescription
modeStringRequired on /run and /book. Use plan for /run; use createItinerary or updateItinerary for /book. /plan forces this value to plan.
problemIdStringCaller-supplied idempotency key. This also becomes the public job ID for polling.
ordersArrayShipment payloads to assign to the optimized tours. Each order should include a stable externalId so Curri can return unassigned reasons.

Optional Input Parameters

ParameterTypeDescription
accountIdNumberNumeric internal account ID. Omit this to use the authenticated user's default account. String account IDs are rejected on the public REST surface.
fleetArrayFleet 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.
targetItineraryIdsArrayExisting itinerary IDs to update when using mode: "updateItinerary".
optionsObjectPer-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.

FieldTypeDescription
externalIdStringStable shipment ID. Used to key unassigned reasons.
pickupAddressAddressWhere the shipment is picked up.
dropoffAddressAddressWhere the shipment is dropped off.
pickupWindowOpenISO 8601 date-timeEarliest pickup time.
pickupWindowCloseISO 8601 date-timeLatest pickup time.
dropoffWindowOpenISO 8601 date-timeEarliest dropoff time.
dropoffWindowCloseISO 8601 date-timeLatest dropoff time.
payloadItemsArrayItems being moved (see Payload Items).
customTagsArrayTags 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.

FieldTypeDescription
addressLine1StringStreet address.
cityStringCity.
stateStringState or region.
postalCodeStringPostal code.
latitudeStringOptional. Decimal degrees, sent as a string.
longitudeStringOptional. Decimal degrees, sent as a string.

Payload Items

Each entry in an order's payloadItems describes one item being moved.

FieldTypeDescription
descriptionStringHuman-readable item description.
heightNumberHeight in inches.
lengthNumberLength in inches.
widthNumberWidth in inches.
weightNumberWeight in pounds.
quantityNumberNumber of identical items.
idStringOptional caller-supplied item ID.
Units

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.

FieldTypeRequiredDescription
driverExternalIdStringYesStable driver ID.
vehicleExternalIdStringYesStable vehicle ID.
vehicleTypeStringYesVehicle type, e.g. cargo-van or box-truck.
scheduledStartISO 8601 date-timeYesWhen the vehicle starts its route.
vehicleSpecObjectNoPer-vehicle capacity and cost overrides.
routeSkills[String]NoSkills this vehicle provides for skill-matched orders.
externalIdStringNoOptional fleet-entry ID.
metaObjectNoArbitrary caller metadata.

Mode Behavior

ModeDescription
planReturns an optimized route without writing to Curri's database. Use for previews, feasibility checks, and customer-facing planning flows.
createItineraryOptimizes and creates new Curri itineraries. Use when the optimized tours should become dispatchable.
updateItineraryOptimizes and updates existing Curri itineraries. Use when reflowing routes that already exist in Curri.

Response Fields

Successful 200 responses return a routing result.

FieldTypeDescription
problemIdStringThe idempotency key for this optimization request.
itinerariesArrayOptimized itinerary objects. In plan mode these may be unpersisted planned itineraries; in book modes these are persisted itinerary-shaped objects.
unassignedArrayOrders 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/run and POST /v1/routing/plan wait up to the sync timeout (30 seconds by default, capped at 120 seconds) for a result. If the optimizer finishes in time you get 200; otherwise you get 202 with a statusUrl.
  • POST /v1/routing/book is fully synchronous and never returns 202.
  • Poll GET /v1/routing/jobs/:problemId until it returns 200. While the job runs it returns 202 with { "status": "processing" }.
  • Completed jobs are retained for roughly 30 minutes. Polling after that returns 404 with code job_not_found.
  • There are no routing webhooks today; use polling to detect completion.

Options Precedence

Routing options are merged in this order:

  1. Curri's default optimization options.
  2. The account's effective optimization settings, including inherited parent account settings.
  3. 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.

OptionTypeAllowed values / notes
objectiveStringOne of MIN_DURATION, MIN_MAX_TRAVEL_COST_DISTANCE, MIN_VEHICLE_AVOID_TOUR_OVERLAP, MIN_VEHICLE_UTILIZATION, or a provider-specific objective string.
trafficModeStringautomatic or none.
enableStopClusteringBooleanGroup nearby stops into a single visit.
volumeOptimizationBooleanFactor payload volume into vehicle packing.
bundlePayloadItemsBooleanKeep an order's payload items together.
tagPrioritiesArray[tag, priority] tuples; priority ranges from 0 to 5.
lockedStopsArray[itineraryExternalId, [stops]] tuples to pin stop order.
vehicleMaps, vehicleSpecOverridesArray[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:

StatusMeaning
400Request shape or routing input validation failed.
401Authentication failed, or the authenticated user has no default account.
403The request accountId does not match the authenticated account.
404Job or routing endpoint not found.
500Queue, optimization, or booking failed. Retry is safe with the same problemId.

Error Codes

CodeStatusMeaning
unauthenticated401No valid JWT or API-key credentials were supplied.
no_account_for_user401The authenticated user has no default account.
invalid_input400The request body failed shape validation.
invalid_routing_input400Routing-specific validation failed (for example, missing fleet).
string_account_id_unsupported400A string accountId was sent; send the numeric ID or omit it.
invalid_job_id400The job id path parameter was missing or empty.
account_mismatch403The request accountId does not match the credential's account.
not_found404Unknown routing endpoint.
job_not_found404No job with that id, or it expired after ~30 minutes.
enqueue_failed500The routing job could not be enqueued.
optimization_failed500The optimizer did not return a usable result.
unknown_job_state500The job ended in an unexpected state.
book_failed500Booking or upsert failed after optimization.
job_lookup_failed500Reading 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.