Skip to main content
POST
Compute Endpoint
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: Every response, whatever the op, carries 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. 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.
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.

Coverage and honesty labels

  • US + Canada. Every response’s notes includes "coverage: US + Canada", regardless of op or mode.
  • Durations reflect typical traffic, not real-time conditions. Every response that actually drives — every screen/labor_shed call, and any distance/nearest call with mode: "driving" — adds "durations reflect typical traffic, not real-time" to notes. Don’t use this for “how long right now”; use it for “how long, typically.”
  • mode: "straightline" (available on distance and nearest only) never calls a routing provider — it’s a pure geodesic (great-circle) distance, computed locally, free. duration_seconds/duration_minutes are always null in 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: Neither returns an error upstream. "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 curated set.
  • Prefer coordinates for anything you resolve repeatedly. They skip the gate, cost nothing to geocode, 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_address in 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: Here’s the gate catching a live case: "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:
A coordinate string skips the gate entirely — there’s no confidence signal on a bare "lat,lng" to gate on.

Per-item failures never fail the whole request — except for a required role

A distance/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 — a notes entry saying how many failed and in which role, plus the resolved_origins / resolved_destinations / resolved_anchors arrays naming which ones and why.
  • nearest — a notes entry 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 for n: 3 can legitimately return two.
  • Any driving leg — flag: "unreachable_or_snapped" with a null duration means no road route was found. Don’t present its distance as drivable.
  • labor_shed — tracts_unreachable counts tracts excluded the same way.

distance — N origins × M destinations

That’s a real measured pair (Brooklyn → JFK): 13.8 mi / 22.2 km driven in about 27.5 minutes of typical traffic. Mixing an address with a coordinate, and a locator that fails to resolve, in one request:
"JFK Airport" is a place name, not an address — see Locators. It resolves as an ordinary unresolvable address string:
Note 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:
(26.3 driven miles against a straight-line distance of about 49 miles — far short of what any real road route to the island would require.) Distances stay visible; 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

There’s no "origin_name"-style search — set names one of six curated destination sets, each backed by a public federal dataset already ingested into the catalog: Two things worth knowing before you rely on this:
  • The search radius is fixed at 160 km (~100 mi) and isn’t configurable in v1. If nothing qualifies within that radius, candidates comes back empty — not an error.
  • applied_filters echoes exactly what you sent, not the default that ran when you sent nothing. If you call @substations with no filters, the 115 kV floor above still applies, but applied_filters reads null. Pass filters explicitly 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.
A non-survivor is never dropped silently. 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

This sums two Census-tract fields — civilian labor force (ACS 5-year, B23025) and total population (CenPop2020) — over every tract reachable from 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_queried is this count. paid_driving_calcs counts only those routes with usable durations; inner-band geometry is free.
Every annulus leg is also checked with the same geodesic snap guard every other /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 is free. 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 free Fetch price, with no /v1/proximity call needed:
Reach for /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

Driving calculations cost $0.02 on Free, $0.018 on Build, and $0.015 on Growth. A calculation is one directed origin-to-destination route that returns a usable duration. Failed resolutions, unavailable routes and snap-guarded routes are excluded. paid_driving_calcs reports the count. For nearest, all successful candidates evaluated to produce the ranking count, including candidates outside the returned top N. For screen, all successful origin-to-anchor pairs count, including routes beyond your chosen time threshold. Straight-line calculations, geocoding and labor-shed estimates are free. There is no minimum charge or address surcharge. Free work still requires an authenticated account and follows request and capacity limits. The maximum cost is reserved before the driving work starts. Unused funds are released when the successful calculation count is known. Labor sheds first run a free geometric prefilter to determine the annulus count. An estimate returns maximum_cost_usd_by_plan; failed routes can reduce the final charge. Use max_cost_usd as a decimal string to limit a request, for example "max_cost_usd": "2.00". The default request ceiling is $10. A reservation that exceeds your ceiling or available funds is refused before driving work. max_credits is no longer accepted. Every successful response includes a dollar receipt:
Send a stable Idempotency-Key header for each logical paid request. Repeating that key with the same request returns its saved result and original receipt without another charge. Reusing a key for changed inputs returns a conflict. With no key, each call is a new operation. Hosted MCP accepts the equivalent idempotency_key argument. A disconnected client can recover a completed operation at GET /v1/operations/{operation_id}. Results remain available for 30 days; financial records persist independently. Free responses have operation_id: null. Failed source execution produces no charge.

Retrying a paid operation

If an otherwise retryable source error or timeout ends a paid operation permanently, the error keeps its original code and HTTP status but includes operation_status: "failed" and retryable: false, with no Retry-After header. Repeating that key cannot restart the work. To try again after the underlying problem is resolved, choose a new idempotency key; a successful new request is billed at the normal price. Hosted MCP returns the same guidance. Free requests and failures before paid execution can still be retryable. Follow the response’s retryable flag instead of deciding from its HTTP status alone. If a payment or database acknowledgement is uncertain, retry the same operation; a new key could start separate work.

Errors

All errors use the standard {"detail": {"error", "message", "retryable"}} shape (see Errors). For rows marked “depends,” see Retrying a paid operation. A confirmed terminal paid failure is never retried under the same key.

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:
  • role — which required role failed: origin, or anchor on screen. 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), or geocoding_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_count is exact; unresolved is 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 alongside unresolved_accuracy_types (the tiers only; the addresses stay off that surface, see the note below).
Note that 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/proximity in v1 — every request completes in the response.
  • Per-request caps: distance/screen origins ≤ 500, distance destinations ≤ 500, screen anchors ≤ 10, nearest n ≤ 25, labor_shed minutes 5–90, annulus ≤ 3,000 tracts. On top of those, distance’s origins × destinations (and screen’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 reject 422 invalid_request in the standard error object — the breached cap and the advice to split are in message, and pydantic’s per-field detail is in errors. 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_share before 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 clears labor_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/proximity down 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

N origins x M destinations, driving or straightline.

destinations
string[]
required
Required array length: 1 - 500 elements
op
string
required
Allowed value: "distance"
origins
string[]
required
Required array length: 1 - 500 elements
max_cost_usd
string | null

Maximum USD to reserve for this request. Defaults to $10; set an explicit decimal string to authorize a different ceiling. Unused reservation is released; only successful driving calculations are charged.

Pattern: ^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$
mode
enum<string>
default:driving
Available options:
driving,
straightline
units
enum<string>
default:miles

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

Response

Successful Response