---
title: "Compute a route between two locations"
method: POST
path: "/v1/directions"
tags: ["Routing"]
---

# Compute a route between two locations

`POST /v1/directions`

## Request

- Base URL: `https://geo.thinair.co`
- URL: `https://geo.thinair.co/v1/directions`
- Auth: HTTP bearer

Generate routes, ETAs, and turn-by-turn directions between locations. Live traffic is post-route ETA enrichment and does not change the returned path; truck traffic adjustments are slowdown-only. Supports auto / truck / bicycle / pedestrian / motorcycle, with hazmat + dimension + toll avoidance for commercial routing. Costs 1 quota unit per call.

## Request body

- DirectionsRequest: Routing request — origin + destination (free text or coordinate pair), optional waypoints, costing model, and per-mode parameters. Traffic is post-route ETA enrichment only; it does not change the selected path.
  - `origin` union, required: Free-text address ('Houston, TX', '4131 Center St, Houston, TX 77007') OR a coordinate pair { lat, lon }.
    - string
    - object
      - `lat` number, required
      - `lon` number, required
  - `destination` union, required: Free-text address ('Houston, TX', '4131 Center St, Houston, TX 77007') OR a coordinate pair { lat, lon }.
    - string
    - object
      - `lat` number, required
      - `lon` number, required
  - `via` union[]: Intermediate stops in order. Same shape as origin/destination. Up to 48 waypoints (max 50 total locations).
    - union: Free-text address ('Houston, TX', '4131 Center St, Houston, TX 77007') OR a coordinate pair { lat, lon }.
      - string
      - object
        - `lat` number, required
        - `lon` number, required
  - `alternates` integer: Number of genuine motorized alternate routes to request (0, 1, or 2). Positive values require no via waypoints.
  - `costing` 'auto' | 'truck' | 'bicycle' | 'pedestrian' | 'motorcycle': Transport mode. Default: auto.
  - `preset` string: Routing preset — one of: car_default, truck_53, truck_box26, truck_tanker, truck_flatbed, truck_step_deck, truck_auto_carrier, truck_sprinter, truck_double, truck_oversize, motorcycle, scooter, bike, walk, transit. Sets costing AND dimensions together. NOTE: these are NOT the `vehicle_profile` names — passing a vehicle_profile id here is not recognised and currently falls back to a CAR route.
  - `vehicle_profile` string: Truck dimension profile (10): DRY_VAN_53, FLATBED_48, FLATBED_40, STEP_DECK, TANKER, BOX_TRUCK_26, AUTO_CARRIER, SPRINTER_VAN, DOUBLE_TRAILER, OVERSIZE. Sets height/weight/length/axles. Distinct from `preset`, which also sets costing.
  - `truck_height` number: Truck height (m)
  - `truck_weight` number: Truck weight (metric tons)
  - `truck_width` number
  - `truck_length` number
  - `hazmat` boolean: Routing avoids hazmat-restricted roads when true.
  - `axle_load` number
  - `axle_count` integer
  - `top_speed` number
  - `use_highways` number
  - `use_ferry` number
  - `avoid_tolls` boolean
  - `include_traffic` boolean: Apply post-route live-traffic ETA enrichment. Default: true. Traffic does not change the returned path (path_influenced=false); truck adjustments are slowdown-only.
  - `include_weather` boolean
  - `include_elevation` boolean: When true, include available route elevation fields. Point elevation may be returned for the origin and destination. Cumulative gain/loss is returned only when route-profile elevation is available. Default: false (an extra upstream lookup, opt-in only).
  - `units` 'miles' | 'kilometers': Distance units. Default: miles. Accepts 'km'/'mi' as aliases.
  - `depart_at` string, date-time: Departure time (ISO 8601). Default: now.
  - `include_geometry` 'none' | 'encoded' | 'full': Route geometry shape control. "none" returns origin/destination Points only. "encoded" returns an encoded polyline6 string in a top-level `geometry` field plus origin/destination Points (≈95% smaller than raw GeoJSON; decode with @mapbox/polyline at precision 6). "full" emits decoded coordinates as a LineString in `geojson.features[0]`. When omitted (default), small routes use "full" and long routes auto-downgrade to "encoded" with a `geometry_truncated` warning.

## Response `200`

Successful route. See `geojson` for FeatureCollection geometry.

- DirectionsResponse: Routing result. `geojson` (FeatureCollection) present when the upstream returned shape geometry. Upstream-emitted fields (`confidence`, `method`, `layer`, `source` on origin/destination resolution) are passed through unchanged. `traffic_provenance` states what traffic did to this specific result.
  - `distance` number, required
  - `distance_units` 'miles' | 'kilometers', required
  - `duration_seconds` number, required
  - `base_duration_seconds` number, required: The routing engine's duration with no traffic applied. Equal to duration_seconds.
  - `effective_duration_seconds` number, required: What to plan against: base_duration_seconds adjusted by any computed traffic delay, else identical to base_duration_seconds.
  - `traffic_delay_seconds` number, required: effective_duration_seconds minus base_duration_seconds. Positive means traffic made the ETA slower, negative means it made the ETA faster, and zero means the duration was unchanged. For truck costing, general traffic evidence is slowdown-only, so this value is never negative.
  - `traffic_adjusted_seconds` number: Computed traffic-adjusted duration in seconds when traffic produced an adjustment. Use effective_duration_seconds as the authoritative duration for this response.
  - `base_arrival_utc` string, required: Arrival time assuming base_duration_seconds, in UTC.
  - `base_arrival_local` string, nullable, required: Same instant as base_arrival_utc, in the destination's local time. Null when no destination timezone was resolved.
  - `effective_arrival_utc` string, required: Arrival time assuming effective_duration_seconds, in UTC.
  - `effective_arrival_local` string, nullable, required: Same instant as effective_arrival_utc, in the destination's local time. Null when no destination timezone was resolved.
  - `has_toll` boolean, required
  - `has_highway` boolean, required
  - `has_ferry` boolean, required
  - `costing` string, required
  - `legs` object[], required
  - `maneuvers` object[], required
  - `maneuver_count` number, required
  - `timing` object, required
    - `departure_utc` string, required: Departure instant, ISO-8601 UTC. Empty string when no route was produced.
    - `departure_local` string, nullable, required: Departure in the ORIGIN's local time. Null when the origin timezone was not resolved — never a UTC instant relabelled as local.
    - `arrival_utc` string, required: Arrival instant, ISO-8601 UTC. Empty string when no route was produced.
    - `arrival_local` string, nullable, required: Arrival in the DESTINATION's local time. Null when the destination timezone was not resolved.
    - `origin_timezone` string, nullable, required: IANA timezone at the origin, or null when unresolved. See origin_timezone_status.
    - `destination_timezone` string, nullable, required: IANA timezone at the destination, or null when unresolved. See destination_timezone_status.
    - `origin_timezone_status` 'resolved' | 'boundary_ambiguous' | 'source_conflict' | 'not_covered' | 'unavailable', required: Why origin_timezone is what it is. Only `resolved` carries a name; the rest explain the null rather than hiding it.
    - `destination_timezone_status` 'resolved' | 'boundary_ambiguous' | 'source_conflict' | 'not_covered' | 'unavailable', required: Why destination_timezone is what it is.
    - `timezone_artifact_version` string, nullable, required: Boundary artifact both timezones came from. Null when none is active.
    - `duration_seconds` number, required: Routed duration in seconds. 0 when no route was produced.
    - `timezones_crossed` string[]: Distinct IANA zones sampled along the route, present ONLY when the route crosses more than one. An unresolved sample is omitted rather than contributing a fabricated crossing.
  - `origin` object, required
  - `destination` object, required
  - `geojson` object
  - `alternates` object[]
    - `distance` number, required
    - `distance_units` 'miles' | 'kilometers', required
    - `duration_seconds` number, required
    - `legs` object[], required
      - `leg` number, required
      - `distance` number, required
      - `duration_seconds` number, required
      - `maneuvers` number, required
    - `maneuvers` object[], required
      - `instruction` string
      - `distance` union
        - number
        - string
      - `time` number
      - `type` number
      - `begin_shape_index` number
    - `geojson` object
      - `type` 'FeatureCollection', required
      - `features` object[], required
        - `type` 'Feature', required
        - `id` union
          - string
          - number
        - `geometry` object, nullable, required
          - `type` union, required
            - 'Point' | 'LineString' | 'Polygon' | 'MultiPoint' | 'MultiLineString' | 'MultiPolygon' | 'GeometryCollection'
            - string
          - `coordinates` unknown
          - `geometries` unknown[]
            - unknown
        - `properties` object
    - `geometry` object
  - `traffic` object: Traffic observations consulted along this route. Sample span distances are always expressed in miles; provider-specific legacy extensions may also be present.
    - `sample_count` integer, required
    - `sample_spacing_mi` number, required: Nominal spacing between traffic observations, in miles, regardless of the route's display units.
    - `samples` object[], required
      - `mile` number, required: Approximate mile marker along the route for this observation.
      - `lat` number, required
      - `lon` number, required
      - `current_speed` number
      - `free_flow_speed` number
      - `congestion` string
      - `units` string
      - `measured_mi` number: Length of road actually measured by this observation, in miles. Omitted when the provider reported no measurable span.
      - `measured_start_mi` number: Start of the measured span as distance along the route, in miles.
      - `measured_end_mi` number: End of the measured span as distance along the route, in miles.
    - `adjusted_time_factor` number
    - `adjusted_eta_seconds` number
    - `error` string
  - `timezone_start` string, nullable, required: IANA timezone at the route origin, or null when unresolved. Contains information from timezone-boundary-builder, which is made available under the Open Database License (ODbL) v1.0. Timezone boundaries © OpenStreetMap contributors. US legal-zone cross-check: U.S. Department of Transportation, Bureau of Transportation Statistics (public domain).
  - `timezone_end` string, nullable, required: IANA timezone at the route destination, or null when unresolved. Contains information from timezone-boundary-builder, which is made available under the Open Database License (ODbL) v1.0. Timezone boundaries © OpenStreetMap contributors. US legal-zone cross-check: U.S. Department of Transportation, Bureau of Transportation Statistics (public domain).
  - `timezone_start_status` 'resolved' | 'boundary_ambiguous' | 'source_conflict' | 'not_covered' | 'unavailable', required
  - `timezone_end_status` 'resolved' | 'boundary_ambiguous' | 'source_conflict' | 'not_covered' | 'unavailable', required
  - `timezone_artifact_version` string, nullable, required
  - `elevation_gain_m` number, nullable, required
  - `elevation_loss_m` number, nullable, required
  - `elevation_gain_ft` number, nullable, required
  - `elevation_loss_ft` number, nullable, required
  - `elevation_start_m` number, nullable, required
  - `elevation_end_m` number, nullable, required
  - `elevation_start_ft` number, nullable, required
  - `elevation_end_ft` number, nullable, required
  - `traffic_provenance` object, required: What traffic actually did to this response: whether it changed the roads chosen, whether it changed the duration, and how fresh the underlying observations were. Always present — when no traffic data was consulted it reports mode `none` rather than being omitted.
    - `mode` 'none' | 'eta_enrichment' | 'candidate_ranking' | 'native_live_routing' | 'native_predicted_routing', required: Which traffic-processing rung this request attempted. `none`: no traffic data was consulted. `eta_enrichment`: after the route was chosen, traffic evidence was sought only to adjust its estimated duration; the roads returned are unchanged. The attempt may fail or produce no adjustment, so inspect `outcome` and `duration_influenced` for what reached this answer. `candidate_ranking`: traffic re-ranked several candidate routes against each other. `native_live_routing`: live speeds were used to choose which roads to take. `native_predicted_routing`: as above, plus time-of-day predicted speeds for a future departure.
    - `outcome` 'TRAFFIC_DISABLED' | 'PROVIDER_DISABLED' | 'QUOTA_EXHAUSTED' | 'NO_SEGMENT_MATCH' | 'INSUFFICIENT_COVERAGE' | 'STALE_EVIDENCE' | 'UPSTREAM_ERROR' | 'APPLIED', required: Neutral, stable reason code for what happened to traffic evidence on this request. `TRAFFIC_DISABLED`: no traffic evidence was sought. `PROVIDER_DISABLED`: evidence was sought but the traffic backend was not configured. `QUOTA_EXHAUSTED`: the traffic backend's spend allotment was exhausted. `NO_SEGMENT_MATCH`: the backend answered but no sample matched a live road segment. `INSUFFICIENT_COVERAGE`: the backend reported no sensor coverage for the sampled area. `STALE_EVIDENCE`: an adjustment was computed, but the evidence behind it was fetched more than 30 minutes ago because the provider could not refresh it. `UPSTREAM_ERROR`: a configured backend or network failure prevented reading all requested traffic evidence. `APPLIED`: an adjustment was computed and reached this response; inspect `retrieved_at` / `retrieval_age_seconds` to see how old the evidence behind it is (acquisition age — this traffic source reports no observation time, see those fields' own descriptions). Material failure codes take precedence over `APPLIED` when only partial evidence adjusted the duration; `duration_influenced` remains true in that case.
    - `path_influenced` boolean, required: Whether traffic could have changed WHICH roads were returned. False today: the route is selected from the road network's own speeds, so traffic never changes the path — only the estimated duration.
    - `duration_influenced` boolean, required: Whether traffic actually changed the duration returned by THIS request. False when no traffic adjustment was applied, even if traffic was requested and fetched successfully.
    - `source_as_of` string, nullable, required: DEPRECATED since 2026-09-24, always null. The traffic source provides no observation timestamp, so this field can never be filled honestly. See `retrieved_at` for when ThinAir fetched the traffic evidence (not when it was observed). Will be removed in /v2.
    - `route_coverage_pct` number, nullable, required: Percentage of the route's distance backed by a live traffic observation. This is a MEASURED figure, not an attributed one: it sums the length of road each traffic observation actually matched and divides by the route distance. Coverage can range from sparse observations to most of a corridor, depending on the evidence available for this request; use the returned value rather than assuming a sampling pattern. A low percentage does not mean the ETA is unreliable; it means most of the route's duration comes from the road network's own speeds rather than from live observation. Null when no live traffic observation carried a measurable length.
    - `freshness_seconds` number, nullable, required: DEPRECATED since 2026-09-24, always null — same reason as `source_as_of`. See `retrieval_age_seconds` for the acquisition age ThinAir CAN report honestly. Will be removed in /v2.
    - `retrieved_at` string, nullable, required: ISO 8601 time when ThinAir fetched the traffic evidence behind this response (point or tile). This is acquisition metadata, not the time the traffic was observed — the traffic source reports no observation time (see `source_as_of`). Null when no consulted sample's acquisition time was determinable.
    - `retrieval_age_seconds` number, nullable, required: Age, in seconds, of `retrieved_at` — how long ago ThinAir fetched the traffic evidence behind this response (point or tile). This is acquisition/cache age, not traffic-observation freshness. Null under the same condition as `retrieved_at`.
    - `routing_release_id` string, nullable, required: Identifier of the road-network build that produced this route. Null today: the routing engine does not report one.
    - `overlay_epoch` string, nullable, required: Identifier of the live-speed overlay applied to the road network. Null today: no live-speed overlay is in use.
  - `meta` object, required

## Other responses

- `400`: Invalid request body.
- `401`: Authentication required or failed.
- `404`: No route found between the origin and destination, or no road near a given coordinate.
- `429`: Quota exceeded for this tenant.
- `502`: Upstream routing engine unavailable.
- `504`: The routing engine exceeded its time budget.

---

[API](https://skmtc.dev/thinair/apis/thinair-geo-api.md) · [All operations](https://skmtc.dev/thinair/apis/thinair-geo-api/llms.txt) · [OpenAPI document](https://skmtc.dev/thinair/apis/thinair-geo-api/revisions/8fac2d5e4b34?raw)
