# Directions, Routing & Distance Matrix API — no API key (`davidar/openstreetmap-directions-api`) Actor

Directions, travel times, distance matrices, route optimization, isochrones, geocoding and elevation on OpenStreetMap: a Google Maps Directions API alternative with no API key. Driving, cycling, walking, truck. Polyline + GeoJSON geometry. MCP-ready for Claude and AI agents.

- **URL**: https://apify.com/davidar/openstreetmap-directions-api.md
- **Developed by:** [David Roberts](https://apify.com/davidar) (community)
- **Categories:** Developer tools, Travel, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.40 / 1,000 route computeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Directions, Routing & Distance Matrix API — turn-by-turn directions, travel times, route optimization, isochrones, geocoding and elevation on OpenStreetMap, no API key

**Directions, Routing & Distance Matrix API** is a keyless alternative to the **Google Maps Directions API** and **Distance Matrix API**, built on **OpenStreetMap**. Send an origin and a destination, as place names or coordinates, and get back **driving, cycling and walking directions** with distance, **travel time**, turn-by-turn instructions and the **full route geometry** as polyline (precision 6 and 5) and GeoJSON. The same Actor answers **distance matrices** (travel time between many origins and destinations), **route optimization** (the best order for delivery stops), **isochrone maps** (how far can I get in 30 minutes), **forward and reverse geocoding** and **elevation profiles**, plus toll/ferry/highway flags and alternative routes. There is no map-provider key to obtain, no second billing account and no terms to click through.

It is built for **AI agents (through MCP), scripts and data pipelines** that need a route now, and for **logistics, delivery pricing, trip planners and commute or catchment analysis** in bulk: up to 1000 routes, a 50×50 matrix, 10 isochrones, 1000 geocodes or 100 elevation profiles per run, one dataset row each.

### What can the Directions, Routing & Distance Matrix API do?

- 🚗 **Directions for 8 travel profiles**: car, bike, foot, truck (with height, weight and hazmat limits), motorcycle, scooter, bus and taxi.
- 🧭 **Multi-stop routes**: up to 20 locations per route, one leg per stop, with up to 3 alternative routes on A→B trips.
- 🗺️ **Geometry that decodes correctly**: `polyline6`, `polyline5` and GeoJSON, all describing the same line. A precision-6 polyline decoded at precision 5 lands in the ocean; here each format is labelled with its precision.
- 🔀 **Optimized trips**: give it the stops and it picks the visiting order for the shortest trip, first and last fixed.
- 🔁 **Time/distance matrices**: up to 50 sources × 50 targets, one row per source.
- ⏱️ **Isochrones**: up to 4 time (minutes) or distance (km) contours per point, as polygons or lines.
- 📍 **Place names or coordinates**: text such as `"Apollo Bay VIC"` is geocoded for you, and the row shows what it matched.
- 📌 **Geocoding on its own**: place names and addresses to coordinates, or coordinates to the nearest addresses and places, up to 10 matches each.
- ⛰️ **Elevation profiles** along a route, or along any line you send (a GPX track, a hiking trail), with total climb and descent.
- 🚫 **Avoid** tolls, highways, ferries, unpaved roads, your own polygons (a flooded valley) or points (a closed bridge).
- 🚲 **Route tuning**: bike type, hill and main-road tolerance, walking speed, shortest instead of fastest.
- 🕘 **Depart-at / arrive-by** times using typical road speeds for that time of day.

Because it runs on Apify, you also get the **REST API**, **scheduling**, **webhooks**, integrations (Make, Zapier, n8n) and the **Apify MCP server** for AI agents, with no extra setup.

### Use cases

- **Delivery ETA and pricing**: distance and drive time from the depot to each customer, priced per kilometre or per minute.
- **Logistics and fleet planning**: a travel-time matrix from every depot to every drop, for assigning jobs to the nearest vehicle.
- **Multi-stop delivery route optimization**: give it the stops and it returns the shortest visiting order (the travelling-salesman problem for up to 20 locations), with the route in that order.
- **Trip planners and itineraries**: a day's drive through named towns, with legs, drive times and turn-by-turn directions.
- **Commute and travel-time analysis**: drive, cycle or walk times from a property or site to work, schools and stations, for property search or site selection.
- **Catchment and service-area maps**: isochrones showing everything within 15, 30 or 60 minutes of a store, clinic or depot.
- **Bulk geocoding**: CRM, event or store-locator addresses to coordinates, and coordinates back to the nearest address.
- **Elevation for cycling, hiking and EV range**: climb, descent and a profile along a route or any GPX track.
- **Truck routing**: routes that respect height, weight and hazmat limits.
- **AI agents**: an assistant connected to the Apify MCP server can answer "how long is the drive from A to B" with one call and cite the distance and time from the row.

### Compared with the Google Maps Directions and Distance Matrix APIs

|                           | This Actor                                                                  | Google Maps Platform                                    | Google Maps scraper Actors on Apify                 |
| ------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------- |
| Price per route           | $0.003                                                                      | $0.005 (Compute Routes Essentials), $0.010 (Routes Pro) | $0.015 + $0.01 setup, or $0.0025 + $0.007 per start |
| Price per matrix element  | $0.0005                                                                     | $0.005                                                  | —                                                   |
| Free usage                | Apify free plan: car routes and matrices from coordinates                   | 10,000 calls a month per SKU                            | Apify free plan credit                              |
| API key / billing account | None; your Apify account                                                    | Google Cloud project, API key and billing account       | None; your Apify account                            |
| Full route geometry       | Yes: polyline6, polyline5 and GeoJSON                                       | Yes: encoded polyline                                   | Not on most                                         |
| Alternative routes        | Up to 3, $0.003 each                                                        | Yes                                                     | Varies                                              |
| Multi-stop routes         | Up to 20 locations                                                          | Yes                                                     | Varies                                              |
| Route optimization        | Yes, 4–20 stops, same price as a route                                      | Waypoint reordering at the Pro price                    | No                                                  |
| Isochrones                | Yes                                                                         | No                                                      | No                                                  |
| Geocoding                 | Yes, $0.001 per result                                                      | Separate Geocoding API                                  | Separate Actors                                     |
| Elevation                 | Yes, along a route or any line                                              | Separate Elevation API                                  | No                                                  |
| Profiles                  | Car, truck (dimensions, hazmat), bike, foot, motorcycle, scooter, bus, taxi | Drive, bicycle, walk, two-wheeler; no truck             | What the Google Maps website offers                 |
| Live traffic              | No: typical speeds for the time of day                                      | Yes                                                     | What the Google Maps website shows                  |
| Public transit            | No                                                                          | Yes                                                     | What the Google Maps website shows                  |
| Data                      | OpenStreetMap                                                               | Google                                                  | Google Maps website                                 |

Google prices are from its March 2025 price sheet; scraper prices are two Store Actors' listed prices on 2026-09-28. Choose Google when you need **live traffic or public transport**. Choose this Actor for **isochrones, route optimization, elevation and full geometry in one call**, no key, and a lower price per route. Road coverage is OpenStreetMap's: excellent in cities, variable on remote tracks.

### What data do you get?

| Field                                     | Row kind  | Meaning                                                                                     |
| ----------------------------------------- | --------- | ------------------------------------------------------------------------------------------- |
| `status`                                  | all       | `ok`, `no_route`, `geocode_failed`, `unsupported` or `unknown` (see [Statuses](#statuses)). |
| `route.distanceM`, `route.durationS`      | route     | Total distance (metres) and travel time (seconds).                                          |
| `route.geometry`                          | route     | `polyline6`, `polyline5` and/or `geojson` (`[lng, lat]` pairs), per the `geometry` option.  |
| `route.legs[].maneuvers[]`                | route     | Turn-by-turn steps: `name` (`turn_right`…), `instruction`, street names, distance, time.    |
| `route.hasToll`, `hasFerry`, `hasHighway` | route     | Whether the route uses a toll road, a ferry or a motorway.                                  |
| `route.bbox`                              | route     | `[minLng, minLat, maxLng, maxLat]`.                                                         |
| `route.elevation`                         | route     | `{intervalM, m[]}`: metres above sea level every `intervalM` metres along the route.        |
| `alternates[]`                            | route     | Alternative routes, same shape as `route`.                                                  |
| `locations[].geocoded`                    | route     | For text locations: the matched label, coordinates, confidence and match type.              |
| `targets[]`                               | matrix    | Per target: `distanceM`, `durationS` (`null` = unreachable) and `via` (`matrix`/`route`).   |
| `geojson`                                 | isochrone | GeoJSON FeatureCollection, one Feature per contour.                                         |
| `optimizedOrder`                          | route     | With `optimize`: the input indices in visiting order.                                       |
| `results[]`                               | geocode   | Matches, best first: `label`, `lat`, `lng`, `confidence`, `layer`, `matchType`.             |
| `samples[]`, `summary`                    | elevation | Per sample `distanceM` and `elevationM`; min, max, climb, descent and length of the line.   |
| `source`                                  | all       | `engine`, `attribution` (display it — see below), `backendId`, `retrievedAt`, `durationMs`. |

### How to get directions from OpenStreetMap in 4 steps

1. Open the Actor's **Input** tab and fill **Origin** and **Destination**, for example `Torquay VIC` and `Apollo Bay VIC`. For many trips at once, use **Routes** instead.
2. Choose the **profile** (`car` by default) and, under **Output**, the geometry format and step detail you want.
3. Click **Start**. A handful of routes finish in a few seconds.
4. Download the dataset as JSON, CSV or Excel, or read it from the API. The **Overview** view shows distance, time, tolls and ferries per row.

For a matrix, isochrones, geocoding or elevation, fill **Matrix**, **Isochrones**, **Geocode** or **Elevation profiles** instead. `mode` is optional: the Actor runs whichever of `routes` (or `origin` + `destination`), `matrix`, `isochrones`, `geocode` or `elevation` you send. Send only one of them per run, or set `mode` to choose.

### Input

#### Quick route: origin, destination, waypoints

For one trip, skip `routes` and send the trip at the top level:

```json
{ "origin": "Torquay VIC", "destination": "Apollo Bay VIC", "profile": "car" }
```

`waypoints` adds stops in order (up to 18, so 20 locations with the ends), and any of the three can be a `"lat,lng"` string instead of a place name:

```json
{
    "origin": "-38.3385,144.3255",
    "destination": "Apollo Bay VIC",
    "waypoints": ["Lorne VIC"],
    "profile": "car"
}
```

This is exactly one route `{"id": "route", "locations": [origin, ...waypoints, destination], "profile": profile}`: same row, same price. `profile` takes the same values as in `routes`. `origin` without `destination`, or the other way round, is an input error. If you also send `routes`, **`routes` wins** and the quick fields are ignored with a warning.

#### Routes: turn-by-turn directions

`routes` is the batch form, for several trips or per-trip options:

```json
{
    "routes": [
        {
            "id": "great-ocean-road",
            "locations": ["Torquay VIC", "Apollo Bay VIC"],
            "profile": "car",
            "alternates": 1
        }
    ],
    "steps": "instructions",
    "elevationIntervalM": 100
}
```

`routes` holds up to **1000** requests. Each takes:

| Key                     | Notes                                                                                                                                                                                                                          |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`                    | Optional, echoed in `input.id`.                                                                                                                                                                                                |
| `locations`             | **2–20** locations: origin, stops in order, destination. Each stop starts a new leg. Routes with more than 20 are skipped with a warning.                                                                                      |
| `profile`               | `car` (default), `bike`, `foot`, `truck`, `motorcycle`, `scooter`, `bus`, `taxi`.                                                                                                                                              |
| `alternates`            | 0–3. Only for a **two-location** route **without** `departAt`/`arriveAt`; otherwise set to 0 with a warning. The engine may return fewer than asked, or none. Each alternate returned is charged as one more `route_computed`. |
| `avoid`                 | Any of `tolls`, `highways`, `ferries`, `unpaved`. `bike` and `foot` support only `ferries`; other values are dropped for those profiles with a warning on the row.                                                             |
| `departAt` / `arriveAt` | One of the two, `YYYY-MM-DDTHH:MM`, local time at the origin, no time zone. Uses typical speeds for that time, not live traffic.                                                                                               |
| `excludePolygons`       | Array of rings, each at least 3 `[lng, lat]` pairs (GeoJSON order). Roads crossing a ring are avoided.                                                                                                                         |
| `truck`                 | `height`, `width`, `length` (metres), `weight`, `axleLoad` (tonnes), `hazmat` (true/false). Used only with `profile: "truck"`; ignored with a warning on other profiles.                                                       |
| `optimize`              | `true` reorders the stops for the shortest trip; see [Optimized routes](#optimized-routes).                                                                                                                                    |
| `avoidLocations`        | Points whose nearest road is avoided (coordinates only, any format below). For a closed bridge or a blocked street.                                                                                                            |
| `tuning`                | Per-profile preferences; see [Route tuning](#route-tuning).                                                                                                                                                                    |

#### Matrix: travel time and distance between many points

```json
{
    "matrix": {
        "sources": [
            [-37.8183, 144.9671],
            [-38.1499, 144.3617],
            [-37.5622, 143.8503]
        ],
        "targets": [
            [-38.3385, 144.3255],
            [-38.54, 143.978],
            [-38.758, 143.6706]
        ],
        "profile": "car",
        "longPairs": "reject"
    }
}
```

`matrix` is one object: `sources` and `targets` (each **1–50** locations), `profile`, optional `departAt`, `id` and `longPairs`. The result is **one row per source**, each with a `targets` array.

The default routing engine caps a single matrix pair at **400 km of road**, so pairs more than 380 km apart as the crow flies are handled before the matrix request (an engine without a per-pair cap answers them as ordinary matrix elements):

- `longPairs: "reject"` (default): if both ends are coordinates, the input is refused before anything runs. If a long pair only shows up after geocoding place names, the matrix rows come back `no_route` with the pairs listed in `error`.
- `longPairs: "route"`: each long pair is computed as an individual route. Those targets carry `via: "route"` and are charged as `route_computed`, not as a matrix element.

#### Isochrones: how far can I get

```json
{
    "isochrones": [
        {
            "id": "roma-st-cycling",
            "location": [-27.4648, 153.019],
            "profile": "bike",
            "minutes": [15, 30],
            "polygons": true
        }
    ]
}
```

`isochrones` holds up to **10** requests: `location`, `profile`, and either `minutes` (up to **4** contours, each ≤ 120) or `km` (up to 4, each ≤ 200). `polygons: true` returns filled polygons; the default `false` returns boundary lines. `denoise` (0–1) drops small islands and `generalize` (metres) simplifies the outline.

#### Optimized routes

```json
{
    "routes": [
        {
            "id": "coast-run",
            "locations": [
                [-38.1499, 144.3617],
                [-38.758, 143.6706],
                [-38.3385, 144.3255],
                [-38.54, 143.978],
                [-38.1499, 144.3617]
            ],
            "optimize": true
        }
    ]
}
```

With `optimize: true` the first and last locations stay put and the stops between them are visited in whatever order makes the trip shortest (4–20 locations; make first and last the same for a round trip). The row's `optimizedOrder` lists the input indices in visiting order — for this coast run you would expect `[0, 2, 3, 1, 4]` (Geelong, Torquay, Lorne, Apollo Bay, back to Geelong) — and `locations` and `legs` follow that order: `legs[i]` runs from `locations[i]` to `locations[i + 1]`, and `optimizedOrder[i]` is the input index of `locations[i]`. With fewer than 4 locations there is nothing to reorder, so `optimize` is ignored with a warning and the trip is routed (and charged) as a plain route; it cannot be combined with `departAt`/`arriveAt`, and alternates are not available. An optimized route is charged as one `route_computed` ($0.003), the same as a plain route.

#### Route tuning

`tuning` passes preferences to the engine for the route's profile. Keys that don't apply to the profile, and unknown keys, are ignored with a warning; an out-of-range value is an input error.

| Key               | Profiles   | Notes                                                                                |
| ----------------- | ---------- | ------------------------------------------------------------------------------------ |
| `bicycleType`     | bike       | `road`, `hybrid`, `cross` or `mountain`: sets typical speed and which surfaces suit. |
| `useRoads`        | bike       | 0 (stick to bike paths) … 1 (roads are fine).                                        |
| `useHills`        | bike, foot | 0 (avoid hills, even at a detour) … 1 (don't care).                                  |
| `walkingSpeedKmh` | foot       | 0.5–25 km/h (engine default 5.1).                                                    |
| `shortest`        | all        | `true` minimises distance instead of time.                                           |
| `roundaboutExits` | all        | `false` drops the separate "take the 2nd exit" maneuvers.                            |

#### Geocoding: place names ↔ coordinates

```json
{
    "geocode": [
        { "id": "lorne", "text": "Lorne VIC" },
        { "id": "flinders-st", "text": "Flinders Street Station, Melbourne", "size": 3 },
        { "id": "near-apollo-bay", "lat": -38.758, "lng": 143.6706, "layers": ["address", "street"], "size": 5 }
    ],
    "geocodeCountry": "AU"
}
```

Each query is **forward** (`text`: a place name or address) or **reverse** (`lat` and `lng`: the addresses and places nearest that point). `size` asks for up to 10 matches (default 1), best first; `layers` restricts them to kinds of place — `address`, `street`, `venue` (or `poi`), `locality`, `localadmin`, `neighbourhood`, `county`, `region`, `postalcode`, `country`, or `coarse` for any administrative area. Forward queries are bounded to `geocodeCountry`. A backend that returns only one reverse result unless `layers` names a single kind of place says so in the row's `warnings` when it reduced your `size`.

Forward results go through the same word-match check as place names in routes: a match is kept only when at least half of your significant words appear in it (see the [FAQ](#why-did-a-place-name-come-back-geocode_failed-when-the-geocoder-found-something)), and the row is `geocode_failed` only when no match passes; `looseGeocoding: true` turns the check off. Reverse results are not checked.

One row per query: `query` (as parsed) and `results[]`. **Each result returned is one `geocoded_location` ("Lookup") event** ($0.001), so `size: 5` can cost up to $0.005; a query with no match is `geocode_failed` and free.

#### Elevation profiles

```json
{
    "elevation": [
        { "id": "great-ocean-road", "polyline": "…", "polylinePrecision": 5, "intervalM": 500 },
        {
            "id": "three-points",
            "coordinates": [
                [144.3255, -38.3385],
                [143.978, -38.54],
                [143.6706, -38.758]
            ]
        }
    ]
}
```

Send a line as an encoded `polyline` (set `polylinePrecision` to how it was encoded: 5, the default, for most tools; 6 for this Actor's `polyline6`) or as `coordinates` (`[lng, lat]` pairs in GeoJSON order, or `{"lat", "lng"}` objects), up to 5000 points. `intervalM: 0` (default) gives the elevation at your points only; 10–5000 resamples the line every that many metres. A line may resample to at most 5000 samples (about 50 km at 10 m, 2,500 km at 500 m); a longer one is skipped with a warning naming the smallest `intervalM` that fits, or split it.

The row has `samples[]` (`lat`, `lng`, `distanceM` along the line, `elevationM`, `null` where there is no data, such as over the sea) and `summary` (`minM`, `maxM`, `gainM` total climb, `lossM` total descent, `lengthM`). One `geocoded_location` ("Lookup") event ($0.001) per answered line, however many samples. A line with no elevation data at any sample comes back `no_route` ("no elevation data along this line") and is not charged. To profile a route, pass its `route.geometry.polyline6` with precision 6, or set `elevationIntervalM` on the route request itself.

#### Output options (all modes)

| Field                | Default        | Notes                                                                                                        |
| -------------------- | -------------- | ------------------------------------------------------------------------------------------------------------ |
| `geometry`           | `all`          | `polyline6`, `polyline5`, `geojson`, `all` or `none`.                                                        |
| `steps`              | `instructions` | `instructions` (maneuvers with text), `maneuvers` (no text), `none` (legs only).                             |
| `units`              | `km`           | `km` or `mi`, for instruction text only. Numeric fields are always metres and seconds.                       |
| `language`           | `en-GB`        | Instruction language, 28 locales from `bg-BG` to `uk-UA`. There is no `en-AU`; `en-GB` is the closest.       |
| `elevationIntervalM` | `0`            | 0 = off, or 10–1000: an elevation in metres every that many metres along each route. 30 is a good choice.    |
| `geocodeCountry`     | `AU`           | Two-letter country code that bounds and biases place-name lookups. Empty string = worldwide.                 |
| `looseGeocoding`     | `false`        | Accept any geocoder hit, even one sharing few words with your query (see the FAQ on `geocode_failed`).       |
| `concurrency`        | `4`            | 1–8 requests in parallel. Requests to the routing engine are capped per host, so values above 4 rarely help. |

#### Location formats

Any location, in any mode, can be:

- an object: `{"lat": -37.8183, "lng": 144.9671}`, with `lon` or `latitude`/`longitude` also accepted;
- a pair: `[-37.8183, 144.9671]` in **`[lat, lng]` order**, unlike GeoJSON and `excludePolygons`;
- a `"lat,lng"` string: `"-37.8183,144.9671"`, **latitude first**. A string whose first number is outside ±90 but would be valid swapped is refused with `looks like lng,lat — give latitude first`;
- place text: `"Lorne VIC"`, `"Geelong"`, `"Ballarat railway station"`.

Text is geocoded against OpenStreetMap and other open address data, bounded to `geocodeCountry`, and the best match is used when at least half of your significant words appear in it (otherwise `geocode_failed`, naming the closest match — see the FAQ). A town name resolves to the town centre: fine between towns, wrong for the last mile. Use an address or coordinates when the exact door matters.

### Output example

A trimmed route row from a real run of the Torquay → Apollo Bay drive, requested with coordinates:

```json
{
    "kind": "route",
    "input": { "id": "great-ocean-road", "index": 0 },
    "status": "ok",
    "profile": "car",
    "locations": [
        { "lat": -38.3385, "lng": 144.3255, "snapped": { "lat": -38.3385, "lng": 144.3255, "sideOfStreet": "right" } },
        { "lat": -38.758, "lng": 143.6706, "snapped": { "lat": -38.758, "lng": 143.6706, "sideOfStreet": "left" } }
    ],
    "route": {
        "distanceM": 91330,
        "durationS": 4756,
        "hasToll": false,
        "hasFerry": false,
        "hasHighway": false,
        "bbox": [143.66897, -38.758124, 144.325383, -38.336874],
        "geometry": {
            "polyline6": "``_chAmo|grGmYf[e@p@[|@G…",
            "polyline5": "…",
            "geojson": { "type": "LineString", "coordinates": [[144.325383, -38.338577], "… 3,310 vertices …"] }
        },
        "legs": [
            {
                "distanceM": 91330,
                "durationS": 4756,
                "maneuvers": [
                    {
                        "type": 10,
                        "name": "turn_right",
                        "instruction": "Turn right onto The Esplanade.",
                        "streetNames": ["The Esplanade"],
                        "distanceM": 85,
                        "durationS": 15,
                        "beginShapeIndex": 24,
                        "endShapeIndex": 28
                    },
                    "… 21 maneuvers in all"
                ]
            }
        ]
    },
    "alternates": [{ "distanceM": 118168, "durationS": 5322, "…": "same shape as route" }],
    "source": {
        "engine": "Valhalla",
        "attribution": "© OpenStreetMap contributors (ODbL)",
        "backendId": "a",
        "retrievedAt": "2026-09-26T05:12:40.118Z",
        "durationMs": 2400
    }
}
```

With place names instead of coordinates, each location also carries what the geocoder matched:

```json
{
    "geocoded": {
        "query": "Apollo Bay VIC",
        "label": "Apollo Bay, VIC, Australia",
        "lat": -38.753405,
        "lng": 143.665293,
        "confidence": 1,
        "matchType": "exact",
        "layer": "locality"
    }
}
```

`beginShapeIndex` / `endShapeIndex` index into the decoded route geometry, so each maneuver can be drawn on its own stretch of road. Maneuver `type` is the engine's numeric code; `name` is the readable form.

**Matrix rows** (`kind: "matrix"`): one per source, with `sourceIndex`, `origin`, and `targets[]` of `{index, location, distanceM, durationS, via}`. **Isochrone rows** (`kind: "isochrone"`): `location`, `contours[]` as requested, and `geojson`.

**Snapping.** On route rows, `locations[].snapped` echoes your input point plus the side of the street the engine started on. On matrix rows, `origin.snapped` and `targets[].location.snapped` are the points actually snapped onto the road network.

The run summary is saved to the key-value store as `OUTPUT`: jobs and rows, counts by status, charged events, total kilometres and hours of `ok` routes, whether the run was on the free tier, and warnings.

#### Statuses

| `status`         | Meaning                                                                                                                                      | Charged |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `ok`             | Answered.                                                                                                                                    | yes     |
| `no_route`       | The engine found no path (an island without a ferry, a point with no road nearby), or a rejected long matrix pair.                           | no      |
| `geocode_failed` | A text location matched nothing.                                                                                                             | no      |
| `unsupported`    | No configured routing engine can serve this profile, size or feature. The reason is in `error`.                                              | no      |
| `unknown`        | The engine or geocoder did not answer (timeout or outage), or your spending limit was reached. Retry later. **Never read it as "no route".** | no      |

### How much does routing cost?

Pay-per-event: you pay for answers, not for compute.

| Event                                                       | Price    | When                                                                                                                                                                                                 |
| ----------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `route_computed`                                            | $0.003   | Each route row with `status: ok`, optimized routes included, plus one per alternative route returned (the engine prices an alternate as a full route). Also each long matrix pair routed separately. |
| `matrix_element`                                            | $0.0005  | Each source→target element of an answered matrix, **unreachable (`null`) elements included**.                                                                                                        |
| `isochrone_computed`                                        | $0.002   | Each answered isochrone, all its contours included.                                                                                                                                                  |
| `geocoded_location` — Lookup (geocode or elevation profile) | $0.001   | Each text location resolved to coordinates; in geocode mode, each result returned; each elevation line answered.                                                                                     |
| `apify-actor-start`                                         | $0.00005 | Once per run, per GB of memory.                                                                                                                                                                      |

Higher Apify plans get a discount:

| Event                | Free and Bronze | Silver  | Gold     |
| -------------------- | --------------- | ------- | -------- |
| `route_computed`     | $0.003          | $0.0027 | $0.0024  |
| `matrix_element`     | $0.0005         | $0.0005 | $0.00045 |
| `isochrone_computed` | $0.002          | $0.0018 | $0.0016  |
| `geocoded_location`  | $0.001          | $0.001  | $0.0009  |

Coordinates you supply are free, and so are `no_route`, `geocode_failed`, `unsupported` and `unknown` rows. Geocodes are charged when a place name resolves — a resolved name is an answer in its own right and the row carries its coordinates whatever happens next; a `geocode_failed` job and a job lost to a backend outage (`unknown`) are not charged.

Examples: the Torquay → Apollo Bay drive above, from place names with one alternate returned, is two routes and two geocodes, $0.008; without the alternate it is $0.005. A 10×10 depot matrix from coordinates is 100 elements, $0.05. The optimized coast run above, from coordinates, is one route, $0.003. A thousand routes between coordinates cost $3. Geocoding 100 addresses (one result each) is $0.10; the Great Ocean Road elevation profile every 500 m is one lookup, $0.001.

**Free Apify plan:** served by the community OpenStreetMap routing server rather than the paid engine, so you get exactly what that server offers and nothing less: car profile, coordinates only (it has no geocoder — place names come back `unsupported`, pass coordinates, for example `"lat,lng"` strings in `origin` and `destination`), routes and matrices, alternates when it has them, no isochrones, no geocoding, no elevation, no optimized routes and no toll/motorway flags. The only limit of ours is politeness towards that shared server: **100 jobs per run**, one request at a time. Results can differ slightly from the paid engine.

### Data & attribution

Routes are computed by a hosted routing engine on **OpenStreetMap** data. Travel times use typical road speeds; **there is no live traffic**, and roadworks or closures count only once OpenStreetMap has them. Road coverage is OpenStreetMap's: excellent in cities, variable on remote tracks.

Each row names the engine that answered in `source.engine` (for example `Valhalla`, or `none` on a row no engine was asked for, such as `unsupported`) and the notice that engine's data licence requires in `source.attribution`. **Wherever you show the results — a map, an app, a report — display the `source.attribution` text of the rows you show.** For OpenStreetMap-based results it is "© OpenStreetMap contributors (ODbL)"; OpenStreetMap data is licensed under the [Open Database License](https://www.openstreetmap.org/copyright). Geocoding also draws on other open datasets (Who's On First, OpenAddresses — for Australian addresses, G-NAF — GeoNames, Foursquare Open Source Places); a row carrying a result from one of them has that dataset's notice appended to `source.attribution`, so showing `source.attribution` covers geocoded places too. Read the attribution from the rows rather than hard-coding it. `source.backendId` is an opaque identifier for the operator's debugging and carries no meaning for users.

### Limits

- **Per run:** 1000 routes, 20 locations per route, a 50×50 matrix, 10 isochrones with 4 contours each, 3 alternates per route, 1000 geocode queries with up to 10 results each, 100 elevation lines of up to 5000 points and 5000 samples after resampling (every 10–5000 m). One mode per run.
- **Matrix pairs over 400 km of road** exceed the default engine's limit; see `longPairs`.
- **Isochrones:** each contour ≤ 120 minutes or ≤ 200 km.
- **Elevation** is in metres whatever `units` says.
- **No `en-AU` instructions.** `en-GB` gives metric units and British spelling.
- **Snapping:** a point off the road network is snapped to the nearest road the profile can use. A point with no road within reach is `no_route`.

### FAQ

#### Do I need a routing or maps API key?

No. The Actor calls the routing engine for you and bills through your Apify account. You need only an Apify account.

#### How do I decode the polyline?

`polyline6` uses 6 decimal places, `polyline5` uses 5 (the precision most decoders assume by default). Pass the matching precision to your decoder, for example `polyline.decode(s, 6)` with the Python `polyline` package, or skip decoding and use `geojson`, which is `[lng, lat]` pairs.

#### Does it include live traffic?

No. `departAt` / `arriveAt` apply typical speeds for that time of day, not live conditions.

#### How do I use it from an AI agent (MCP), n8n, Make or my own code?

Through the Apify MCP server (`mcp.apify.com`) an agent can search for this Actor, read this README and call it with just `origin` and `destination`. From code, call the Actor through the [Apify API](https://docs.apify.com/api/v2), the Python or JavaScript client, or the Make, Zapier and n8n integrations. For agents:

- Join rows on `input.id` (echoed) or `input.index` (0-based position). Matrix rows also carry `sourceIndex`, and each target its `index`.
- Check `status` before reading `route`; `route` is absent on non-`ok` rows.
- `geometry: "none"` plus `steps: "none"` keeps rows tiny when only distance and time matter. A long drive's polyline is tens of kilobytes.
- Fetch full items rather than projected fields: some dataset clients silently drop arrays of objects (legs, targets) from field projections.
- Read the row count from the dataset (`totalItemCount`), not the run summary, which can lag by a few rows right after `SUCCEEDED`.

#### What does `unknown` mean, and should I retry?

`unknown` means the engine did not answer in time or returned a server error. It is not charged and is safe to retry. It never means there is no route.

#### Why did a place name come back `geocode_failed` when the geocoder found something?

Geocoders match on words: "Nowhereville XYZ" resolves, with full confidence, to a business called "XYZ Finance" 900 km away. To stop that turning into a routed, billed 1,900 km trip, a hit is accepted only when at least half of your significant words appear in the matched place name (state names, country and abbreviations such as St or Rd don't count). Otherwise the row is `geocode_failed` and `error` names the closest match. If you really do mean that place, pass its coordinates, or set `looseGeocoding: true` to accept any hit.

#### Why is a geocode result I expected missing?

In geocode mode each forward result passes the word-match check separately, so with `size: 5` you may get fewer than five. Set `looseGeocoding: true` to see everything the geocoder returned, or narrow with `layers`.

#### Can I optimize the order and get alternative routes?

No: an optimized route is one route in the chosen order. Route the reordered stops again without `optimize` if you need alternates for a particular leg.

#### Why does a row say `unsupported`, or carry a warning about elevation or departure time?

The actor can be configured with more than one routing backend, and not every backend has every feature. A job goes to the first backend that can serve it; when none can, the row is `unsupported` and `error` says why: `text locations need geocoding, which no routing backend serving this run supports — pass coordinates instead`, `departAt is not supported by the backend`, or a profile that no backend offers. Elevation is never a reason to refuse a job: if the backend that answered has no elevation data, the route is returned without it and the row warns `elevationIntervalM ignored: elevation is not available on the backend that answered`. Some backends also cannot say whether a route uses tolls or motorways; those rows carry `hasToll` / `hasHighway` as `null` with a warning, never `false`.

#### Why did my free-plan run stop, drop routes, or say `unsupported`?

Free Apify plans are served by the community routing server, which has no geocoder, no isochrones and only a car profile; jobs it cannot serve come back `unsupported` with the reason. Runs are capped at 100 jobs to stay polite to that shared server. Everything else — long routes, alternates, matrices — works as it does for paying users. See [pricing](#how-much-does-routing-cost).

### Other Actors by the same author

- [AU Mobile Coverage](https://apify.com/davidar/au-mobile-coverage) grades a route against Telstra, Optus and TPG predicted coverage. Feed it `route.geometry.polyline6` with `polylinePrecision: 6`.
- [AU Address Enrichment](https://apify.com/davidar/au-address-enrichment) adds planning, flood, bushfire, NBN, transport, school-zone and electorate data to Australian addresses.

Found a problem or need a custom routing workflow? Open an issue on the Actor's **Issues** tab.

# Actor input Schema

## `origin` (type: `string`):

Quick route: where the trip starts — a place name or address (`"Torquay VIC"`, `"Flinders Street Station, Melbourne"`) or coordinates as `"lat,lng"` (`"-38.3385,144.3255"`). With `destination` this makes one route; leave both empty to use `routes` (batch), `matrix`, `isochrones`, `geocode` or `elevation` below. Place names are geocoded (one `geocoded_location` event each).

## `destination` (type: `string`):

Quick route: where the trip ends — a place name or address, or `"lat,lng"`. Needs `origin`.

## `waypoints` (type: `array`):

Quick route: optional stops between origin and destination, in visiting order, up to 18 — each a place name or `"lat,lng"`. Each stop starts a new leg. To reorder stops for the shortest trip, use `routes` with `optimize: true` instead.

## `profile` (type: `string`):

Quick route: how you travel. `car` (default), `bike`, `foot`, `truck`, `motorcycle`, `scooter`, `bus`, `taxi`. Free Apify plans: `car` only (served by the community router).

## `mode` (type: `string`):

What to compute. Optional (a quick route with `origin`/`destination` needs no mode): when left out, the Actor runs whichever one of `routes`, `matrix`, `isochrones`, `geocode`, `elevation` you send (send only one, or set this). `route` reads `routes`, `matrix` reads `matrix`, `isochrone` reads `isochrones`, `geocode` reads `geocode`, `elevation` reads `elevation`; the others are ignored. Free (non-paying) Apify users are served by the community routing server: car profile, coordinates only (no geocoding), routes and matrices, no isochrones and no elevation profiles, at most 100 jobs per run.

## `routes` (type: `array`):

Mode `route`, the batch form of the quick route above. Up to 1000 routes per run (free Apify users: at most 100 jobs per run, served by the community routing server). Each item: `id` (optional, echoed in `input.id`); `locations` (2–20: origin, stops, destination — `"Apollo Bay VIC"`, `{"lat": -38.3385, "lng": 144.3255}`, `[lat, lng]` or `"lat,lng"`; routes with more than 20 are skipped); `profile` (`car` default, `bike`, `foot`, `truck`, `motorcycle`, `scooter`, `bus`, `taxi`); `alternates` (0–3, only for a 2-location route without `departAt`/`arriveAt`, otherwise set to 0 with a warning); `avoid` (any of `tolls`, `highways`, `ferries`, `unpaved`; `bike` and `foot` support only `ferries`, other values are dropped with a warning); `departAt` or `arriveAt` (one of them, `YYYY-MM-DDTHH:MM`, local time at the origin, no time zone; typical speeds, not live traffic); `excludePolygons` (rings of at least 3 `[lng, lat]` pairs, GeoJSON order; roads crossing them are avoided); `truck` (`height`, `width`, `length` in metres, `weight`, `axleLoad` in tonnes, `hazmat` true/false; only with profile `truck`); `optimize` (true = reorder the stops between the first and last location for the shortest trip; needs 4–20 locations (with 3 there is nothing to reorder), ignored with a warning on fewer and routed and charged as a plain route; cannot be combined with `departAt`/`arriveAt`; sets `alternates` to 0; the row gets `optimizedOrder`; charged as one `route_computed`, like a plain route); `avoidLocations` (coordinates only, any coordinate format, place names are an input error; the road nearest each point is avoided — a closed bridge, a blocked street); `tuning` (per-profile preferences: `bicycleType` `road`/`hybrid`/`cross`/`mountain` and `useRoads` 0–1 for `bike`; `useHills` 0–1 for `bike` and `foot`, 0 avoids hills; `walkingSpeedKmh` 0.5–25 for `foot`; `shortest` true to minimise distance instead of time; `roundaboutExits` false to drop roundabout exit maneuvers; keys that do not apply to the profile, and unknown keys, are dropped with a warning; out-of-range values are input errors). See the README for examples.

## `matrix` (type: `object`):

Mode `matrix` (paid Apify plans only). One object: `sources` and `targets` (each 1–50 locations, same formats as routes), `profile` (default `car`), `departAt` (`YYYY-MM-DDTHH:MM`, optional), `longPairs` (`reject` default, or `route`), `id` (optional). The default engine caps a single matrix pair at 400 km of road (an engine without that cap answers such pairs in the matrix). With `reject`, a pair of coordinates more than 380 km apart as the crow flies fails the input up front (a long pair found only after geocoding makes the rows `no_route`); with `route`, each such pair is computed as a separate route and charged as `route_computed`. Output is one row per source; each element is charged as `matrix_element`, unreachable ones included.

## `isochrones` (type: `array`):

Mode `isochrone` (paid Apify plans only). Up to 10 per run. Each item: `location` (one location), `profile` (default `car`), exactly one of `minutes` (up to 4 contours, each ≤ 120) or `km` (up to 4, each ≤ 200), `polygons` (true for filled polygons, false for boundary lines; default false), `denoise` (0–1), `generalize` (metres, ≥ 0, Douglas-Peucker tolerance), `id` (optional). Output is one row per isochrone with a GeoJSON FeatureCollection.

## `geocode` (type: `array`):

Mode `geocode` (paid Apify plans only). Up to 1000 queries per run, one row each. Each item is either forward — `text` (a place name or address, e.g. `"Lorne VIC"`) — or reverse — `lat` and `lng` (what is at this point); exactly one of the two. `size` (1–10, default 1): results per query, best first. `layers` (optional): restrict to kinds of place, any of `address`, `street`, `venue` (or `poi`), `locality`, `localadmin`, `neighbourhood`, `county`, `region`, `postalcode`, `country`, `coarse` (all administrative areas). `id` (optional, echoed). Forward queries are bounded to `geocodeCountry`, and each result must pass the same word-match check as route place names (at least half of your significant words in the match) unless `looseGeocoding` is on; the row is `geocode_failed` only when no result passes. Reverse results are not checked. Each result returned is one `geocoded_location` event; a `geocode_failed` query is free.

## `elevation` (type: `array`):

Mode `elevation` (paid Apify plans only). Up to 100 lines per run, one row each. Each item: either `polyline` (an encoded polyline; set `polylinePrecision` to 5 or 6 to match how it was encoded — default 5, as most tools encode; a route row's `route.geometry.polyline6` needs 6) or `coordinates` (`[lng, lat]` pairs in GeoJSON order, or `{"lat": …, "lng": …}` objects), not both; up to 5000 points. `intervalM`: 0 (default) = one sample per given point, or 10–5000 = a sample every that many metres along the line; at most 5000 samples per line (a longer one is skipped with a warning naming the smallest intervalM that fits). `id` optional. The row has `samples[]` (`lat`, `lng`, `distanceM` along the line, `elevationM`, null where there is no data, e.g. over the sea) and a `summary` (min, max, total climb and descent, length). One `geocoded_location` ("Lookup") event per answered line; a line with no elevation data at any sample is `no_route` and free.

## `geometry` (type: `string`):

Geometry format(s) on route rows. `polyline6` is encoded polyline at 6 decimal places (the engine's native precision); `polyline5` is 5 decimal places, the precision most decoders assume by default; `geojson` is a LineString of `[lng, lat]`. `all` returns all three, `none` drops geometry (smaller rows). Decode each polyline at its own precision.

## `steps` (type: `string`):

`instructions` returns maneuvers with narrative text; `maneuvers` returns them without text; `none` returns legs with distance and duration only.

## `units` (type: `string`):

Units used in instruction text. Numeric fields are always metres (`distanceM`) and seconds (`durationS`).

## `language` (type: `string`):

Language of the instruction text, one of the 28 listed locales. The engine has no `en-AU`; `en-GB` (default) is the closest (metric, British spelling).

## `elevationIntervalM` (type: `integer`):

0 = no elevation profile. Otherwise 10–1000: an elevation in metres every this many metres along each route (30 is a good choice). Values 1–9 are rejected. Always metres, whatever `units` says.

## `geocodeCountry` (type: `string`):

ISO 3166-1 alpha-2 code (two letters, e.g. `AU`, `NZ`, `GB`) that bounds and biases place-name lookups, in text route locations and in forward `geocode` queries. Empty = worldwide. Reverse geocoding and coordinates are never bounded by it.

## `looseGeocoding` (type: `boolean`):

Accept any geocoder hit for a place name, even when none of your words appear in the matched place. Off by default: a name like "Nowhereville XYZ" otherwise matches a venue called "XYZ Finance" and gets routed. When off, such hits are dropped; a location or `geocode` query left with none comes back as geocode\_failed naming the closest match. Reverse geocoding is never filtered.

## `concurrency` (type: `integer`):

Requests processed in parallel (1–8). Requests to the routing engine are capped per host regardless of this value, so raising it past 4 rarely helps.

## Actor input object example

```json
{
  "origin": "Torquay VIC",
  "destination": "Apollo Bay VIC",
  "profile": "car",
  "mode": "route",
  "geometry": "all",
  "steps": "instructions",
  "units": "km",
  "language": "en-GB",
  "elevationIntervalM": 0,
  "geocodeCountry": "AU",
  "looseGeocoding": false,
  "concurrency": 4
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset rows discriminated by kind (route | matrix | isochrone | geocode | elevation). Route rows: route.{distanceM, durationS, hasToll, hasFerry, hasHighway, bbox, geometry, legs, elevation}, alternates\[], locations\[], optimizedOrder\[] (optimize: true). Matrix rows: one per source with targets\[].{index, distanceM, durationS, via}. Isochrone rows: geojson FeatureCollection. Geocode rows: query.{kind, text | lat, lng}, results\[].{label, lat, lng, confidence, layer}. Elevation rows: samples\[].{lat, lng, distanceM, elevationM}, summary.{minM, maxM, gainM, lossM, lengthM}. status is ok | no\_route | geocode\_failed | unsupported | unknown; only ok is charged, and unknown is a fetch failure, never no route. source.attribution must be displayed with the results.

## `summary` (type: `string`):

OUTPUT record: jobs (requests parsed), rows (dataset rows written), byStatus {ok, geocode\_failed, no\_route, unsupported, unknown}, charged {route\_computed, matrix\_element, isochrone\_computed, geocoded\_location} (event counts; optimized routes count as route\_computed, elevation profiles as geocoded\_location), totalKm and totalHours (ok route rows), freeTier (run served as a free Apify plan, capped at 100 jobs), warnings\[] (input and backend notes), retrievedAt (ISO timestamp).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "origin": "Torquay VIC",
    "destination": "Apollo Bay VIC",
    "profile": "car",
    "mode": "route"
};

// Run the Actor and wait for it to finish
const run = await client.actor("davidar/openstreetmap-directions-api").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "origin": "Torquay VIC",
    "destination": "Apollo Bay VIC",
    "profile": "car",
    "mode": "route",
}

# Run the Actor and wait for it to finish
run = client.actor("davidar/openstreetmap-directions-api").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "origin": "Torquay VIC",
  "destination": "Apollo Bay VIC",
  "profile": "car",
  "mode": "route"
}' |
apify call davidar/openstreetmap-directions-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,davidar/openstreetmap-directions-api"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/1Sth5hhczkoOuXcAU/builds/ob3ArCw2mmalLNOU1/openapi.json
