open-distance
API Reference

One endpoint. No key. Standard JSON.

Everything you need to call open-distance — the request, the response, the limits, and exactly how it differs from the legacy distance-matrix API it mirrors.

No API key required Base · open-distance.com Coverage · lower 48 + DC Format · Distance Matrix JSON

Overview

A serverless distance & duration API on the edge, response-compatible with the legacy Distance Matrix JSON shape. It returns scalar distance and duration — no route geometry, no turn-by-turn, no live traffic.

The public endpoint takes no key= and needs no signup. It’s rate-limited per IP. Forks that want private auth can re-introduce it in src/auth.ts.

!

Provided as-is, no warranty — built from public open data, no live traffic. Not for safety- or contract-critical use (emergency dispatch, regulated SLAs, legal billing). Use a commercial API there.

Endpoint

A single GET request. Origins and destinations are pipe-separated; each item is a lat,lng pair or an address string.

GET/maps/api/distancematrix/json
request
# no key — just GET it
curl "https://open-distance.com/maps/api/distancematrix/json\
  ?origins=Stanford,CA|Palo+Alto,CA\
  &destinations=San+Francisco,CA|Oakland,CA\
  &units=imperial"

Parameters

ParamRequiredDefaultNotes
originsyesA|B|… — each a lat,lng pair or address string.
destinationsyesC|D|… — same format as origins.
unitsnoimperialimperial or metric.
modenodrivingOnly driving is supported; other modes are treated as driving.
keynoNot required. Public endpoint is rate-limited per IP.
!

Max 25 elements (origins × destinations) per request, or you’ll get MAX_ELEMENTS_EXCEEDED. place_id: inputs return NOT_FOUND.

Response shape

Byte-compatible with the legacy Distance Matrix JSON, plus a few additive fields (*_matches, data_version, copyrights) that old clients simply ignore.

200 OK · application/json
{
  "origin_addresses":      ["Stanford, CA, USA", "Palo Alto, CA, USA"],
  "origin_matches":        ["rooftop", "rooftop"],
  "destination_addresses": ["San Francisco, CA", "Oakland, CA"],
  "destination_matches":   ["rooftop", "interpolated"],
  "data_version":          "2026-06",
  "rows": [{ "elements": [{
    "distance": { "text": "33.4 mi", "value": 53752 },   // metres
    "duration": { "text": "41 mins", "value": 2460 },  // seconds
    "status": "OK"
  }]}],
  "copyrights": "… ODbL §4.3 produced-work notice …",
  "status": "OK"
}

Match tiers

Each origin and destination reports how confidently it was geocoded, in the origin_matches / destination_matches arrays.

  • rooftopExact mapped point — authoritative rooftop dataset (NAD or OpenAddresses).
  • interpolatedEstimated along a street segment (TIGER) — likely off ~30–100 m.
  • coordsYou passed lat,lng directly; no geocode performed.
  • "" (empty)Geocode failed; the address echoes your raw input.
!

Addresses that only resolve to a ZIP/city centroid (often miles off) return NOT_FOUND rather than a confidently-wrong distance.

Status codes

Top-level statusMeaning
OKRequest valid; see per-element statuses.
INVALID_REQUESTMissing or malformed parameters.
MAX_ELEMENTS_EXCEEDEDorigins × destinations > 25.
OVER_QUERY_LIMITPer-IP rate limit hit (HTTP 429), or the account-wide daily cap hit (HTTP 503 + Retry-After).
REQUEST_DENIEDRequest refused.

Per-element status

Element statusMeaning
OKdistance + duration returned.
NOT_FOUNDOrigin or destination couldn’t be geocoded confidently.
ZERO_RESULTSNo route found between the snapped graph nodes.

Rate limits

Per-IP on the hosted deployment. There is no paid tier — need more headroom? Self-host and tune the env vars (set any to 0 to disable that window). The daily budget is metered in elements (elements = origins × destinations — the same definition Google uses), so it tracks serving cost; the per-second tier is a request burst guard.

WindowLimitEnv var
Second (burst)5 requestsRL_PER_SEC
Day2,500 elementsRL_ELEMENTS_PER_DAY
!

On top of the per-IP limits there's an account-wide cap of 25,000 elements/day (resets at 00:00 UTC). If the whole service hits it you'll get HTTP 503 with a Retry-After header — not a 429. Need higher or dedicated limits? Email hello@open-distance.com for custom solutions.

Self-throttle headers

Every response — 200 and 429 alike — carries limit headers so you can back off without an extra probe. The -Day values are element budgets.

response headers
X-RateLimit-Limit-Second:     5
X-RateLimit-Remaining-Second: 4
X-RateLimit-Limit-Day:        2500 # elements
X-RateLimit-Remaining-Day:    2499 # elements left today
# on 429: Retry-After, X-RateLimit-Tier: sec | day

Deviations from the legacy API

It mirrors the legacy distance-matrix wire format, with intentional differences:

AreaDifference
AuthNo key=. Rate-limited per IP instead.
NumbersFrom a routed graph with no live traffic — they differ from traffic-aware providers.
Omitted fieldsNo fare, duration_in_traffic, geocoded_waypoints, or warnings.
Added fieldsorigin_matches, destination_matches, copyrights, data_version — all additive.
ModesDriving only. place_id: inputs → NOT_FOUND.
CachingSuccessful responses send Cache-Control: public, max-age=3600.

Coverage

Continental US — the lower 48 states + DC. Short and medium routes go through a tiled local-road graph; long-haul routes (anything over ~620 mi straight-line) route through a national highway overlay layer that holds the country’s motorway network in a single binary. NYC→LA, Seattle→Miami, and Boston→Houston all return real answers.

Live coverage, sources, and supported behaviours are reported by the GET /coverage endpoint.

Self-hosting

The whole thing is fork-and-deploy on your own edge account for the entire continental US. The pipeline (fetch → build → upload → publish) is driven by one script.

The cost model is expensive once, then cheap for years: roughly ~$500 one-time for the full data load (it's write-heavy on D1), then ~$5/month hosting + ~$200/year maintenance to serve ~750k–1M queries/month — the entire lower-48. Honest about the nasty setup; the long horizon is cheap.

Full setup, data sources, and the refresh pipeline live in the repo. Read the self-host guide →