Skip to main content
POST
Multi-coordinate drive-time compute: distance, nearest, screen, labor shed
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.

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: Geocodio’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 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_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 legflag: "unreachable_or_snapped" with a null duration means no road route was found. Don’t present its distance as drivable.
  • labor_shedtracts_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 exactly this count, and it is paid_driving_calcs for labor_shed — you’re charged for the tracts that actually needed a routed answer, not the ones geometry already settled.
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 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:
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

Every response echoes paid_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:
Floors exist so a straightline or memo-served request never bills $0, and so a 1-tract labor_shed doesn’t undercharge for the fixed cost of running it: Address-form locators cost extra, additively. Any origin/destination/ anchor that isn’t a "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: Credits are debited from your plan’s shared pool — the same one every other endpoint draws from. /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 (+1 per 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.
The geocoding part is debited before the op runs, so any failure that happens after your locators were resolved has already paid for those lookups. “Not billed” below therefore always means the driving part is not billed — a request made entirely of coordinates costs literally nothing in those rows, while one carrying address-form locators still owes 1 credit per address: One rule covers almost the whole table: you pay for upstream work we actually performed on your behalf. A bad locator costs the one geocode it took to discover it was bad, not the matrix it would have fed.
503 geocodio_distance_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).

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.
  • errorlow_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 — Geocodio’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.

op
string
required
Allowed value: "distance"
origins
string[]
required
Required array length: 1 - 500 elements
destinations
string[]
required
Required array length: 1 - 500 elements
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
max_credits
integer | null

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 <= 200000

Response

Successful Response