Multi-coordinate drive-time compute: distance, nearest, screen, labor shed
curl --request POST \
--url https://api.example.com/v1/proximity \
--header 'Content-Type: application/json' \
--data '
{
"op": "distance",
"origins": [
"<string>"
],
"destinations": [
"<string>"
],
"mode": "driving",
"units": "miles",
"max_credits": 100000
}
'import requests
url = "https://api.example.com/v1/proximity"
payload = {
"op": "distance",
"origins": ["<string>"],
"destinations": ["<string>"],
"mode": "driving",
"units": "miles",
"max_credits": 100000
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
op: 'distance',
origins: ['<string>'],
destinations: ['<string>'],
mode: 'driving',
units: 'miles',
max_credits: 100000
})
};
fetch('https://api.example.com/v1/proximity', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/v1/proximity",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'op' => 'distance',
'origins' => [
'<string>'
],
'destinations' => [
'<string>'
],
'mode' => 'driving',
'units' => 'miles',
'max_credits' => 100000
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/v1/proximity"
payload := strings.NewReader("{\n \"op\": \"distance\",\n \"origins\": [\n \"<string>\"\n ],\n \"destinations\": [\n \"<string>\"\n ],\n \"mode\": \"driving\",\n \"units\": \"miles\",\n \"max_credits\": 100000\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/v1/proximity")
.header("Content-Type", "application/json")
.body("{\n \"op\": \"distance\",\n \"origins\": [\n \"<string>\"\n ],\n \"destinations\": [\n \"<string>\"\n ],\n \"mode\": \"driving\",\n \"units\": \"miles\",\n \"max_credits\": 100000\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/v1/proximity")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"op\": \"distance\",\n \"origins\": [\n \"<string>\"\n ],\n \"destinations\": [\n \"<string>\"\n ],\n \"mode\": \"driving\",\n \"units\": \"miles\",\n \"max_credits\": 100000\n}"
response = http.request(request)
puts response.read_body{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}Resolve
POST /v1/proximity
Drive-time distance, nearest-candidate, proximity screening, and labor-shed compute across multiple US + Canada coordinates.
POST
/
v1
/
proximity
Multi-coordinate drive-time compute: distance, nearest, screen, labor shed
curl --request POST \
--url https://api.example.com/v1/proximity \
--header 'Content-Type: application/json' \
--data '
{
"op": "distance",
"origins": [
"<string>"
],
"destinations": [
"<string>"
],
"mode": "driving",
"units": "miles",
"max_credits": 100000
}
'import requests
url = "https://api.example.com/v1/proximity"
payload = {
"op": "distance",
"origins": ["<string>"],
"destinations": ["<string>"],
"mode": "driving",
"units": "miles",
"max_credits": 100000
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
op: 'distance',
origins: ['<string>'],
destinations: ['<string>'],
mode: 'driving',
units: 'miles',
max_credits: 100000
})
};
fetch('https://api.example.com/v1/proximity', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/v1/proximity",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'op' => 'distance',
'origins' => [
'<string>'
],
'destinations' => [
'<string>'
],
'mode' => 'driving',
'units' => 'miles',
'max_credits' => 100000
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/v1/proximity"
payload := strings.NewReader("{\n \"op\": \"distance\",\n \"origins\": [\n \"<string>\"\n ],\n \"destinations\": [\n \"<string>\"\n ],\n \"mode\": \"driving\",\n \"units\": \"miles\",\n \"max_credits\": 100000\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/v1/proximity")
.header("Content-Type", "application/json")
.body("{\n \"op\": \"distance\",\n \"origins\": [\n \"<string>\"\n ],\n \"destinations\": [\n \"<string>\"\n ],\n \"mode\": \"driving\",\n \"units\": \"miles\",\n \"max_credits\": 100000\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/v1/proximity")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"op\": \"distance\",\n \"origins\": [\n \"<string>\"\n ],\n \"destinations\": [\n \"<string>\"\n ],\n \"mode\": \"driving\",\n \"units\": \"miles\",\n \"max_credits\": 100000\n}"
response = http.request(request)
puts response.read_body{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}Previously referred to as
/v1/compute in early design docs. The endpoint
was renamed to /v1/proximity before its first public release, so there is no
/v1/compute to migrate from — nothing ever served under that path. If you
find compute in our module names or internal symbols, that is the
implementation package, not an alternate endpoint.When to use this
/v1/fetch and /v1/ask answer questions about one coordinate.
/v1/proximity answers questions about the relationship between coordinates
— how far, how long, which is closest, which sites clear a drive-time bar,
how many workers live within a commute. It’s a matrix operation, not a
per-point lookup.
One request body, discriminated on op:
op | Answers |
|---|---|
distance | Driving (or straight-line) distance and duration for every origin × destination pair. |
nearest | The closest N candidates from a curated set (airports, substations, …), ranked by drive time. |
screen | Which origins are within a drive-time band of ANY of up to 10 anchors — and which missed, and by how much. |
labor_shed | Civilian labor force + population reachable from one origin within N drive-time minutes. |
paid_driving_calcs (exactly what
pricing charged for — see Pricing) and notes (the honesty labels
below). Errors share one taxonomy — see Errors.
Porting from the MCP tool? The request shape differs. The 422 names the discriminator, so it is diagnosable — but the wrapper is
invisible until you hit it. REST does not also accept the wrapped form: one
documented shape per surface beats two shapes to keep in sync forever.
mireye_proximity
takes its request as a named req argument, so an MCP call is wrapped. REST
takes the same object as the body directly, so the op fields sit at the top
level.// REST — 200
{ "op": "screen", "origins": [...], "anchors": [...], "max_minutes": 60 }
// REST — 422 union_tag_not_found on discriminator `op`
{ "req": { "op": "screen", ... } }
Coverage and honesty labels
- US + Canada. Every response’s
notesincludes"coverage: US + Canada", regardless of op or mode. - Durations reflect typical traffic, not real-time conditions. Every
response that actually drives — every
screen/labor_shedcall, and anydistance/nearestcall withmode: "driving"— adds"durations reflect typical traffic, not real-time"tonotes. Don’t use this for “how long right now”; use it for “how long, typically.” mode: "straightline"(available ondistanceandnearestonly) never calls a routing provider — it’s a pure geodesic (great-circle) distance, computed locally, free.duration_seconds/duration_minutesare alwaysnullin this mode (no traffic note either, since nothing was timed), because there’s no route to time.
Locators: a coordinate or a street address — never a place name
Every origin, destination, and anchor is either a"lat,lng" string or a US
street address. It is never a place name. “JFK Airport” and “the Tesla
Gigafactory” are not addresses, and passing one resolves as an ordinary
(unresolvable) address string — it does not fall back to a search.
To reach named infrastructure, use nearest’s curated set parameter
(@airports, @substations, @power_plants, @rail, @ports,
@urban_areas — see nearest) or supply
that place’s own coordinate directly.
This is deliberate, not a missing feature: the routing provider’s distance API only ever
receives coordinates that /v1/proximity has already resolved server-side —
never a caller’s raw locator string. That’s what keeps one bad address from
failing an entire N×M matrix (a single bad geocode used to be able to 422 the
whole request), what lets the same accuracy gate /v1/geocode enforces apply
uniformly here, and why a POI name can’t quietly resolve to whatever a text
search happens to guess.
Be as precise as you can — vague locators fail quietly upstream
The failure mode to design against is not “no match found.” It’s a confident match on the wrong place. Two real examples:| You send | Geocoder matches | What it is |
|---|---|---|
"1412 market street" | Market, WV 26411 | a town in West Virginia named Market |
"SFO airport" | Airport, NC 28219 | a town in North Carolina named Airport |
"SFO airport" comes back at confidence
1.0. There is no second candidate to compare against — asking for five
returns exactly one. The only thing standing between that and a 2,400-mile
drive time reported as fact is our accuracy gate, which refuses both because
they resolved to place tier rather than to a street or a rooftop.
So the gate saves you, but only from the coarse cases. Give it as much as you
have:
- Always include city + state, or a ZIP. A bare street line is the single most common cause of a wrong match — Market Streets and Main Streets exist in hundreds of towns, and the geocoder will happily pick one.
- Never send a landmark, business, or airport name. Use its coordinate, or
nearest’s curatedset. - Prefer coordinates for anything you resolve repeatedly. They skip the gate, cost no geocoding credit, and can’t drift.
- A gate pass is not a correctness guarantee. US territories are a known
hole: a Puerto Rico address can match a different PR address at rooftop
tier and 0.99 confidence. Check
formatted_addressin the response against what you sent whenever the stakes are high.
If you fill in a missing piece yourself, say so. An LLM agent can often
repair
"1412 market street" to "1412 Market St, San Francisco, CA" by
inferring the city from surrounding context — a plain program can’t. That
repair is useful, and it is invisible unless you disclose it. Our response
will carry a real rooftop-accuracy address and look authoritative even though
the city came from you, not from your user. Report the assumption alongside
the answer (“the address had no city; I assumed San Francisco”), and ask
rather than guess when you aren’t confident. The 422 carries this rule as
caller_guidance so it’s readable at the point of failure.The accuracy gate
An address resolves through the same floor/v1/geocode enforces — below
parcel/street-tier precision or below 0.8 similarity, it’s refused rather than
guessed. Every locator’s outcome is echoed back as a ResolvedPoint:
error | Meaning | lat/lng |
|---|---|---|
| (absent) | Resolved. Coordinate strings resolve instantly; addresses carry formatted_address/accuracy_type/accuracy from the match. | populated |
unresolvable_input | The provider found no match at all. | null |
low_confidence_resolution | The provider matched something, but it failed the accuracy gate. formatted_address/accuracy_type describe what was rejected — we don’t echo a coordinate we don’t trust. | null |
geocoding_failed | A transient, auth, quota, or unsupported-address-form failure on this one item. | null |
"1450 Ridgecrest Dr, CA" names a
street and a state but no city, and the upstream doesn’t report no match — it
silently returns the centroid of the city of Ridgecrest, which is a real
place hundreds of miles from most streets of that name:
{
"query": "1450 Ridgecrest Dr, CA",
"lat": null,
"lng": null,
"formatted_address": "Ridgecrest, CA 93555",
"accuracy_type": "place",
"accuracy": null,
"error": "low_confidence_resolution"
}
"lat,lng" to gate on.
Per-item failures never fail the whole request — except for a required role
Adistance/screen request’s bulk destinations degrade per-item: a
failed one is omitted from legs, everything else still computes, and each
one’s outcome is echoed in resolved_destinations. nearest’s candidates
degrade too, but with less detail — they are excluded from the ranking and
counted in notes, with no per-candidate echo. paid_driving_calcs is
still priced from the full request shape, not from what happened to resolve —
see Pricing for why that matters.
A required role — every op’s origin(s), or screen’s anchors — fails the
whole request with 422 unresolvable_input if every item in that role fails.
A
200 can be missing rows you asked for. Send 5 destinations, have 2
fail, and you get back 3 legs and no error. Every drop is announced in
notes, so read it before summarising a result — “the 3 nearest” is a false
statement if 2 of the 5 never resolved. What you get per op:distance/screen— anotesentry saying how many failed and in which role, plus theresolved_origins/resolved_destinations/resolved_anchorsarrays naming which ones and why.nearest— anotesentry counting candidates excluded for having no usable road route. There is no per-candidate echo on this op, so the count is the only signal: asking forn: 3can legitimately return two.- Any driving leg —
flag: "unreachable_or_snapped"with anullduration means no road route was found. Don’t present its distance as drivable. labor_shed—tracts_unreachablecounts tracts excluded the same way.
distance — N origins × M destinations
{
"op": "distance",
"origins": ["40.630973,-73.97228"], // 1–500 locators
"destinations": ["40.6413,-73.7781"], // 1–500 locators
"mode": "driving", // "driving" | "straightline", default "driving"
"units": "miles" // reserved; both distance_miles and distance_km are always returned
}
{
"op": "distance",
"legs": [
{
"origin_index": 0,
"destination_index": 0,
"distance_miles": 13.8,
"distance_km": 22.2,
"duration_seconds": 1650,
"duration_minutes": 27.5,
"flag": null
}
],
"resolved_origins": [
{"query": "40.630973,-73.97228", "lat": 40.630973, "lng": -73.97228, "formatted_address": null, "accuracy_type": null, "accuracy": null, "error": null}
],
"resolved_destinations": [
{"query": "40.6413,-73.7781", "lat": 40.6413, "lng": -73.7781, "formatted_address": null, "accuracy_type": null, "accuracy": null, "error": null}
],
"paid_driving_calcs": 1,
"notes": ["coverage: US + Canada", "durations reflect typical traffic, not real-time"]
}
{
"op": "distance",
"origins": ["350 5th Ave, New York, NY 10118"],
"destinations": ["JFK Airport", "40.6413,-73.7781"]
}
"JFK Airport" is a place name, not an address — see
Locators.
It resolves as an ordinary unresolvable address string:
{
"op": "distance",
"legs": [
{ "origin_index": 0, "destination_index": 1, "distance_miles": 13.9, "distance_km": 22.4, "duration_seconds": 1680, "duration_minutes": 28.0, "flag": null }
],
"resolved_origins": [
{"query": "350 5th Ave, New York, NY 10118", "lat": 40.748377, "lng": -73.984854, "formatted_address": "350 5th Ave, New York, NY 10118", "accuracy_type": "rooftop", "accuracy": 1.0, "error": null}
],
"resolved_destinations": [
{"query": "JFK Airport", "lat": null, "lng": null, "formatted_address": null, "accuracy_type": null, "accuracy": null, "error": "unresolvable_input"},
{"query": "40.6413,-73.7781", "lat": 40.6413, "lng": -73.7781, "formatted_address": null, "accuracy_type": null, "accuracy": null, "error": null}
],
"paid_driving_calcs": 2,
"notes": ["coverage: US + Canada", "durations reflect typical traffic, not real-time"]
}
legs has one entry (the destination that resolved), but
paid_driving_calcs is 2 — one origin × two destinations, the request’s
shape, computed before either locator was resolved. You’re charged for what
you asked for, not for what happened to work.
The snap guard
A driven leg shorter than 95% of the straight-line distance between the same two points is not a real road route — it’s the provider silently snapping an unreachable point (an island with no bridge, a spot in open water) to the nearest road. This is a real, verified case: Los Angeles to Catalina Island — no road or bridge crosses that water, and the routing provider still returns a confident 200 with a “drive.”/v1/proximity computes
the straight-line distance locally for every leg and flags this as
impossible rather than passing that through:
{ "origin_index": 0, "destination_index": 0, "distance_miles": 26.3, "distance_km": 42.3, "duration_seconds": null, "duration_minutes": null, "flag": "unreachable_or_snapped" }
duration_seconds/duration_minutes are nulled,
because a duration for a route that doesn’t exist isn’t a number worth
trusting. (Real routes legitimately run longer than straight-line — a
15–20% detour factor is normal — so the guard only fires on the geometrically
impossible direction.)
nearest — top-N from a curated set
{
"op": "nearest",
"origin": "40.630973,-73.97228",
"set": "@airports",
"n": 3, // 1–25, default 3
"filters": null, // set-specific override, see below
"mode": "driving" // "driving" | "straightline"
}
{
"op": "nearest",
"origin": {"query": "40.630973,-73.97228", "lat": 40.630973, "lng": -73.97228, "formatted_address": null, "accuracy_type": null, "accuracy": null, "error": null},
"candidates": [
{
"name": "JOHN F KENNEDY INTL",
"lat": 40.6413, "lng": -73.7781,
"attributes": {"facility_type": "airport", "use": "PU"},
"distance_miles": 13.8, "distance_km": 22.2,
"duration_seconds": 1650, "duration_minutes": 27.5
}
// ...up to n candidates, ranked by duration ascending
],
"applied_filters": null,
"paid_driving_calcs": 15,
"notes": ["coverage: US + Canada", "durations reflect typical traffic, not real-time"]
}
"origin_name"-style search — set names one of six curated
destination sets, each backed by a public federal dataset already ingested
into the catalog:
set | Backing data | Default class filter | Override |
|---|---|---|---|
@airports | FAA NASR (28-day cycle) | Public-use airports only — facility_type == "airport" (heliports excluded) and use is "PU" or unrated (a row with no use value is INCLUDED; only an explicit "PR" is excluded). | none in v1 |
@substations | EIA/HIFLD power substations | max_voltage_kv >= 115. A substation with no published voltage is excluded — it can’t prove it clears the threshold. | {"min_kv": <n>}, e.g. {"min_kv": 0} to see every rated substation (unrated ones stay excluded regardless). |
@power_plants | EIA power plants | none — every in-range plant is a candidate. | — |
@rail | BTS NTAD rail network | none. | — |
@ports | BTS maritime ports | none. | — |
@urban_areas | Census TIGER urban areas | none. | — |
- The search radius is fixed at 160 km (~100 mi) and isn’t configurable in
v1. If nothing qualifies within that radius,
candidatescomes back empty — not an error. applied_filtersechoes exactly what you sent, not the default that ran when you sent nothing. If you call@substationswith nofilters, the 115 kV floor above still applies, butapplied_filtersreadsnull. Passfiltersexplicitly if you need the response to say what threshold was used.
paid_driving_calcs for nearest is min(25, n × 5) — a fixed multiple of
n, not the number of candidates actually found or returned, so it’s
knowable before the call.
screen — proximity filter against up to 10 anchors
Filters a batch of origins by drive-time proximity to any of up to 10
anchors — e.g., “which of these 40 parcels are within 20 minutes of I-95’s
Exit 12 (40.71,-73.99) AND at least 10 minutes from downtown.” Anchors are
"lat,lng" coordinates or street addresses, the same as every other locator
in this API — screen has no curated set parameter (that’s nearest’s),
so there is no shortcut for “the nearest interstate on-ramp”; supply the
on-ramp’s own coordinate.
{
"op": "screen",
"origins": ["40.70,-73.95", "40.75,-73.80", "40.90,-73.70"],
"anchors": ["40.6413,-73.7781"], // 1–10 locators
"max_minutes": 30,
"min_minutes": null // optional lower bound
}
{
"op": "screen",
"survivors": [
{"origin_index": 0, "best_anchor_index": 0, "best_duration_seconds": 1080, "best_duration_minutes": 18.0}
],
"screened_out": [
{"origin_index": 1, "best_duration_seconds": 2220, "best_duration_minutes": 37.0},
{"origin_index": 2, "best_duration_seconds": null, "best_duration_minutes": null}
],
"resolved_origins": [ "…" ],
"resolved_anchors": [ "…" ],
"paid_driving_calcs": 3,
"notes": ["coverage: US + Canada", "durations reflect typical traffic, not real-time"]
}
screened_out reports each
origin’s own best duration against ANY anchor, even though it missed the
band — that’s the near-miss, and it’s the whole reason max_minutes/
min_minutes are enforced locally rather than sent to the routing provider:
an upstream duration filter makes a failing leg vanish with no marker (and
still bills for it), which is unusable for “how close did it come.”
best_duration_seconds: null (the third origin above) means every anchor
leg was unreachable or snapped — not just slow.
min_minutes is a lower bound — use it to exclude an origin that’s too
close to every anchor (e.g., you want 10–30 minutes from a highway, not right
on top of it). screen always drives; there is no straightline mode, since
“screen by proximity” without traffic-aware duration isn’t the use case.
paid_driving_calcs = len(origins) × len(anchors) — the full matrix, always
computed, regardless of how many survive.
labor_shed — civilian labor force + population within a drive time
{
"op": "labor_shed",
"origin": "32.9,-96.9",
"minutes": 45 // 5–90
}
{
"op": "labor_shed",
"origin": {"query": "32.9,-96.9", "lat": 32.9, "lng": -96.9, "formatted_address": null, "accuracy_type": null, "accuracy": null, "error": null},
"civilian_labor_force": 612340,
"population": 1024500,
"tracts_counted": 812,
"tracts_matrix_queried": 40,
"tracts_unreachable": 1,
"minutes": 45,
"paid_driving_calcs": 40,
"notes": ["coverage: US + Canada", "durations reflect typical traffic, not real-time"]
}
origin within minutes of driving. Rather than routing every tract in the
country, it classifies each candidate tract by pure geometry first, which is
free:
- Definitely reachable — the tract’s straight-line distance is under a 20 mph bound. No real route can be slower than a straight line at that speed over that short a distance, so it counts without a routed check.
- Definitely unreachable — straight-line distance exceeds a 75 mph bound. No road beats a straight line, so it’s excluded without a routed check.
- The annulus — everything between those two bounds is genuinely
uncertain and gets one real driving-matrix call per tract centroid.
tracts_matrix_queriedis exactly this count, and it ispaid_driving_calcsforlabor_shed— you’re charged for the tracts that actually needed a routed answer, not the ones geometry already settled.
/v1/proximity driving op applies: a leg shorter than 95% of its
straight-line distance is a snap-to-nearest-road artifact (an island tract
snapped to a nearby mainland road, say), not a real route — even when its
reported duration looks in-budget. A flagged tract is excluded from both
sums and from tracts_counted, but it’s still counted in
tracts_matrix_queried (it was still queried) and in tracts_unreachable,
so a trimmed shed is visible rather than silently over-counted.
A labor_shed query over an entirely near or entirely far origin — a small
minutes in a sparse area, say — can cost zero paid driving calcs and
still bill the 25-credit floor. The annulus is capped at 3,000 tracts;
past that, the request fails loud with 422 shed_too_large rather than
running an enormous, slow matrix.
civilian_labor_force skips any tract whose ACS estimate is null (a sample
the Census Bureau couldn’t produce contributes nothing); population counts
every included tract regardless — a tract with unknown labor force still has
known people living in it.
Just want your own tract’s numbers, not a shed?
The same two figures are also ordinary catalog fields —tract_civilian_labor_force
and tract_population — fetchable for a single point via /v1/fetch, at the
standard per-field credit price, with no /v1/proximity call needed:
curl -s https://api.mireye.com/v1/fetch \
-H "Authorization: Bearer $MIREYE_API_TOKEN" \
-H 'content-type: application/json' \
-d '{"lat": 32.9, "lng": -96.9, "fields": ["tract_civilian_labor_force", "tract_population"]}' | jq
{
"fields": {
"tract_civilian_labor_force": {
"value": 1800,
"unit": "people",
"source": "CENSUS_TRACT_WORKFORCE",
"source_url": "https://www.census.gov/programs-surveys/acs/data/summary-file.html",
"confidence": "medium",
"dataset_vintage": "ACS 2019-2023 5-year civilian labor force (B23025) + 2020 Census population-weighted tract centroid (CenPop2020) population",
"notes": "tract_civilian_labor_force for Census tract 48113017103"
},
"tract_population": {
"value": 3120,
"unit": "people",
"source": "CENSUS_TRACT_WORKFORCE",
"confidence": "medium"
}
}
}
/v1/fetch when you want the home tract’s own figures; reach for
labor_shed when you want the total across everywhere reachable within a
drive time, which is almost never just the home tract.
Pricing
Every response echoespaid_driving_calcs — priced from the request
shape (or, for labor_shed, the annulus size after the free geometric
prefilter), never from how many locators happened to resolve, so the price is
always knowable in advance:
credits = max(op_floor, 12 × paid_driving_calcs) + 1 × address-form locators
labor_shed doesn’t undercharge for the fixed cost of running it:
op | Floor (credits) |
|---|---|
distance | 2 |
nearest | 2 |
screen | 5 |
labor_shed | 25 |
"lat,lng" coordinate triggers its own forward-geocoding
call against the same shared quota POST /v1/geocode draws from, so it’s
priced at that same rate: +1 credit per address-form locator, on top of
the driving-calc price above — a coordinate locator never adds anything.
Capped at 25 address-form locators per request (across
origins+destinations, or origins+anchors on screen) — a request over that
cap is rejected 422 before any billing, same as the total-calc cap below.
Worked examples:
| Request | paid_driving_calcs | Credits charged |
|---|---|---|
distance, 1×1, mode: "straightline" | 0 | 2 (floor) |
distance, 1×1, mode: "driving" | 1 | 12 |
distance, 10 origins × 20 destinations, driving | 200 | 2,400 |
distance, 1×1, driving, 1 address-form destination | 1 | 13 (12 + 1) |
nearest, default n: 3, mode: "straightline" | 0 | 2 (floor) |
nearest, default n: 3 | 15 (min(25, n×5)) | 180 |
screen, 20 origins × 3 anchors | 60 | 720 |
labor_shed, entirely inside the 20 mph inner band | 0 | 25 (floor) |
labor_shed, 40 tracts in the annulus | 40 | 480 |
/v1/meta/plans publishes the constants above
(proximity_per_driving_calc, proximity_distance_min, proximity_nearest_min,
proximity_screen_min, proximity_labor_shed_min, proximity_per_address_locator)
alongside your plan’s credit-to-dollar rate.
Which failures are billed. The price is debited in two parts, and there
are no refunds, so what a failure costs depends on which parts it reached:
- the geocoding part (
+1per address-form locator) is debited before your locators are resolved, because resolving them is what makes those calls; - the driving part (
max(op_floor, 12 × paid_driving_calcs)) is debited as late as it safely can be: after every required locator has resolved, after the curated set has been looked up, and after the per-request budget share has been checked — but still before the driving matrix is called.
| Failure | Driving part | Geocoding part |
|---|---|---|
422 invalid_request (schema, including the per-request caps) | No | No — rejected before the handler runs at all. |
429 proximity_busy | No | No — the overload gate precedes all metering. |
422 unresolvable_input (any op) | No — the matrix never ran. | Yes — those lookups are exactly what discovered the failure. |
422 unknown_set on nearest | No — a bad set ref does no upstream work. | Yes, if the origin was an address. |
422 shed_too_large on labor_shed | No — the annulus is sized by a free geometric prefilter. | Yes, if the origin was an address. |
422 proximity_request_exceeds_budget_share | No — refused before it reserves anything. | Yes, if any locator was an address. |
503 proximity_data_unavailable | No — a missing backing asset is our failure. | Yes, if any locator was an address. |
502/504 from the routing provider (upstream error, deadline) | Yes, not refunded — the call was placed. | Yes |
503 proximity_budget_exhausted | Yes, not refunded — see below. | Yes |
503 proximity_budget_exhausted is the one exception, and it is
deliberate. Our fleet-wide monthly routing budget being exhausted means no
call reaches the provider — yet the driving part is still billed, because the
debit is placed before the budget reservation on purpose. Reversing that
order would let a caller who is over their own credit cap reserve, and waste,
shared budget on every request. Since the error is retryable with
Retry-After: 3600, do not retry it in a tight loop — each attempt is
billed. Wait out the window.Errors
All errors use the standard{"detail": {"error", "message", "retryable"}}
shape (see Errors).
| Error code | HTTP | Retryable | Meaning |
|---|---|---|---|
invalid_request | 422 | no | The body doesn’t match the schema — a bad field, an unknown op, or one of the Limits below. detail.errors carries pydantic’s per-field detail. |
unresolvable_input | 422 | no | Every locator for a required role (an op’s origin(s), or screen’s anchors) failed to resolve. Carries per-locator diagnostics — see below. |
unknown_set | 422 | no | nearest’s set isn’t one of the six curated sets. |
shed_too_large | 422 | no | labor_shed’s annulus exceeded 3,000 tracts. Shrink minutes. |
proximity_request_exceeds_budget_share | 422 | no | This one request would reserve more than its allowed share of the shared monthly driving-matrix budget. The pool is not empty — the request is too big a bite of it, so retrying it unchanged will always fail. The message states the ceiling in driving calcs; split the batch. |
proximity_busy | 429 | yes | Per-worker overload gate. Retry-After: 5. Never billed — the gate is checked before metering. |
proximity_budget_exhausted | 503 | yes | The shared, fleet-wide monthly driving-matrix budget is exhausted. Retry-After: 3600. |
proximity_data_unavailable | 503 | yes | A backing local asset (a curated set, or the labor-shed tract table) isn’t available right now. |
proximity_unconfigured | 503 | no | The service is missing its routing-provider credential. An operator problem, not a bad request. |
upstream_transient | 502 | yes | The routing provider had a transient failure (5xx, connection drop, rate limit). |
upstream_auth | 502 | no | The routing provider rejected the credential or the request. Not your bug to fix by retrying. |
upstream_error | 502 | yes | Catch-all for a routing-provider failure the codes above don’t name — a malformed distance-matrix response, or an undocumented status. Worth reporting if it persists. |
proximity_deadline_exceeded | 504 | yes | The request exceeded the endpoint’s own deadline — distinct from a routing-provider timeout, which stays upstream_transient. |
Diagnosing unresolvable_input
By far the most common way to hold this endpoint wrong is to name a place
rather than an address — "detroit", or a bare street like
"1820 meadowbrook circle" (whose street type matches the town of Circle,
Montana). Those don’t fail loudly at the geocoder: they match
something, just not a place specific enough to route from. The accuracy gate
refuses them rather than answering about the wrong location, and the 422
tells you exactly what it matched so you can fix it in one edit:
{
"detail": {
"error": "unresolvable_input",
"message": "every origin failed to resolve: 1 of 1 matched only to place-level accuracy, which is not specific enough to identify a location. supply a full street address (house number, street, city, state) or a 'lat,lng' coordinate — a bare street name, city, or place name is not specific enough, and a landmark's NAME is never resolvable (reach named infrastructure through a curated `set` or its own coordinate)",
"retryable": false,
"role": "origin",
"unresolved_count": 1,
"unresolved": [
{
"index": 0,
"query": "1820 meadowbrook circle",
"error": "low_confidence_resolution",
"matched_address": "Circle, MT 59215",
"accuracy_type": "place"
}
],
"caller_guidance": "This request could not be resolved as sent. You may retry with a more specific locator. If any part of that locator is something you INFERRED rather than something the caller supplied — a city, state, ZIP, or a coordinate substituted for a place name — you MUST say so explicitly when you report the result…"
}
}
role— which required role failed:origin, oranchoronscreen. Only one role is reported; a request can fail on either.index— position in the list you sent, so you can map a failure back to the locator that caused it.error—low_confidence_resolution(matched, but too coarse to trust — the usual case),unresolvable_input(no match at all), orgeocoding_failed(the lookup itself failed; worth a retry).matched_address/accuracy_type— what the geocoder actually landed on, and how precisely. No coordinate is echoed: the gate refused it, so returning it would invite you to use a point we don’t trust.unresolved_countis exact;unresolvedis capped at 10 entries so a bulk request returns a readable sample rather than one entry per locator.caller_guidance— the disclosure rule above, carried on the error so an agent reads it at the moment it’s about to retry. On the MCP surface the same key appears alongsideunresolved_accuracy_types(the tiers only; the addresses stay off that surface, see the note below).
matched_address and query appear in the response body only —
never in message, which is retained in our request telemetry.
If the ambiguity is the point of your question — you have a name and want to
find out which place it means — use POST /v1/lookup
first. It returns ranked candidates and an explicit clarify disposition
instead of picking one, then feed the coordinate it gives you into
/v1/proximity. That’s also the pattern that scales: resolving 40 candidate
sites once and computing over coordinates is cheaper and more predictable than
re-geocoding them on every call.
Limits
- Sync only. No batch/async job endpoint for
/v1/proximityin v1 — every request completes in the response. - Per-request caps:
distance/screenorigins ≤ 500,distancedestinations ≤ 500,screenanchors ≤ 10,nearestn≤ 25,labor_shedminutes5–90, annulus ≤ 3,000 tracts. On top of those,distance’s origins × destinations (andscreen’s origins × anchors) must not exceed 10,000 — the routing provider’s synchronous distance-matrix limit — and no more than 25 address-form locators (see Pricing) may appear in one request. Both reject422 invalid_requestin the standard error object — the breached cap and the advice to split are inmessage, and pydantic’s per-field detail is inerrors. These two are rejected before the handler runs, so they are not billed (see Pricing for the ones that are). - Per-request share of the shared driving budget: 3,500 driving calcs.
Separate from the caps above and enforced against the actual upstream
spend, after the 30-day memo has been consulted — so a request whose legs
are already memoized can exceed 3,500 total calcs and still pass, because
it buys nothing new. A request that would newly reserve more than that
rejects
422 proximity_request_exceeds_budget_sharebefore any upstream call, naming the ceiling and how to shrink the request. It is never silently truncated, and it is not billed — the check runs before the debit (see the billing table under Pricing). The ceiling clearslabor_shed’s 3,000-tract annulus cap, so a shed at the documented maximum always fits. This exists so no single caller can drain the shared monthly budget and take/v1/proximitydown for everyone else; the message is explicit that the pool still has room. - A driving leg is memoized for 30 days, keyed on the rounded coordinate pair — a repeat lookup of the same origin/destination within the window is served from the memo and still bills at the same rate (the memo is margin, not a discount; pricing is charged from the request shape either way). A failed lookup is never memoized, so a transient outage can’t freeze a wrong answer for a month.
- Coverage is US + Canada, driving only — every driving response says so
in
notes.
Body
application/json
- DistanceOp
- NearestOp
- ScreenOp
- LaborShedOp
N origins x M destinations, driving or straightline.
Allowed value:
"distance"Required array length:
1 - 500 elementsRequired array length:
1 - 500 elementsAvailable options:
driving, straightline Reserved for a future single-unit rendering. Leg always returns BOTH distance_miles and distance_km regardless of this value -- no behavior currently depends on it.
Available options:
miles, km Refuse this request if it would cost more than this many credits. Defaults to the service ceiling (see MIREYE_PROXIMITY_CREDIT_CEILING); raise it to opt into an expensive request, lower it to cap your own spend. The refusal is a 422 that states the exact price, and it happens BEFORE the driving matrix is charged.
Required range:
1 <= x <= 200000Response
Successful Response