Maps API for AI Agents: Routes and Travel Times
Language models guess at distances and travel times. Give your AI agent a routing tool and a travel-time tool instead, with code for both.
Language models guess at distances and travel times. Give your AI agent a routing tool and a travel-time tool instead, with code for both.
CarpathianBot is the crawler behind Carpathian Search. What it fetches, the robots.txt rules it follows, how to block it, and how results are ranked.
Let your AI agent search the web: tool calling with a free web search API for LLMs, the agent loop, prompt injection defenses, and the limits to plan for.
Everywhere you can reach in 15 minutes, drawn as a shape. How isochrones are computed, what changes them, and how to put one on a map.
Which robots.txt names collect text for AI training and which index pages for search, with a file you can copy and the limits of what it can stop.
Ask a language model how long it takes to walk between two addresses and it will give you a confident number, but that number is a guess. A model has no map, no road network and no bus timetable, so it estimates from whatever its training data said about similar places. To answer travel questions correctly, an AI agent needs a maps API it can call as a tool: one tool that returns a route with its time and distance, and one that returns the area reachable from a point within a given time.
This guide shows both tools wired to the Carpathian Maps API, which answers walk, bike, drive and transit routes and travel-time areas from your backend, free up to a monthly allowance. The design applies to any routing API.
A model can describe a city, but it can't measure one. Distances along roads, one-way streets, a river with two bridges and a bus that runs every half hour all change the answer, and none of that sits in a model's weights in a form it can compute with.
A maps tool moves the measurement out of the model and into an engine built for it. The model decides what to ask, the routing engine works out the answer, and the model explains it in words. That's the same split that makes a model good with a calculator: let it reason about the question and let a tool do the arithmetic.
Questions an agent with a maps tool can answer:
Two tools cover most travel questions. Keep them narrow, because a model chooses tools better when each one does one thing.
get_route takes an origin, a destination and a mode (walk, bike, drive or transit), and returns the time, the distance and the turn-by-turn steps. It maps to POST /v1/route.reachable_area takes an origin, a mode and up to four travel times in minutes, and returns the area someone can reach within each. It maps to POST /v1/isochrone.Here's how you might describe them to the model. These schemas are an example to adapt, not part of the API:
[
{
"name": "get_route",
"description": "Travel time, distance and directions between two points. Points are latitude and longitude.",
"parameters": {
"type": "object",
"properties": {
"origin": { "type": "object", "properties": { "lat": { "type": "number" }, "lng": { "type": "number" } }, "required": ["lat", "lng"] },
"destination": { "type": "object", "properties": { "lat": { "type": "number" }, "lng": { "type": "number" } }, "required": ["lat", "lng"] },
"mode": { "type": "string", "enum": ["walk", "bike", "drive", "transit"] }
},
"required": ["origin", "destination", "mode"]
}
}
]
reachable_area follows the same shape with origin, mode and a minutes array of one to four whole numbers from 1 to 120.
Your backend holds the key and makes the call. Return only what the model needs to reason with, and keep the rest for your interface.
import os
import requests
MAPS = "https://maps.carpathian.ai/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["MAPS_API_KEY"]}
def get_route(origin, destination, mode):
r = requests.post(
f"{MAPS}/route",
json={"origin": origin, "destination": destination, "mode": mode},
headers=HEADERS,
timeout=20,
)
if r.status_code == 422:
return {"error": r.json()["error"]}
r.raise_for_status()
route = r.json()["route"]
return {
"minutes": round(route["duration_s"] / 60),
"km": round(route["distance_m"] / 1000, 1),
"steps": [step["instruction"] for step in route["steps"]],
}
The full answer also carries path, the line to draw as latitude and longitude pairs, and for each step a maneuver for an arrow and, on transit, the bus line with departure and arrival times. A model doesn't need a few hundred coordinates to say "about 25 minutes", so drawing the path is your interface's job.
A travel-time area, or isochrone, is the shape on a map someone can reach from a point within a set time. The API returns each one as a GeoJSON polygon in longitude, latitude order, largest area first.
Don't hand the polygon to the model. A model can't reliably tell whether a point sits inside a shape described by hundreds of coordinates. Test it in code and give the model the answer:
from shapely.geometry import Point, shape
def stores_within(origin, minutes, stores):
r = requests.post(
f"{MAPS}/isochrone",
json={"origin": origin, "mode": "drive", "minutes": [minutes]},
headers=HEADERS,
timeout=30,
)
r.raise_for_status()
areas = r.json()["isochrones"]
if not areas:
return []
area = shape(areas[0]["geometry"])
return [s["name"] for s in stores if area.contains(Point(s["lng"], s["lat"]))]
One call answers the question for every store at once, which is faster and cheaper than asking for a route to each.
The Carpathian Maps API answers coordinates, not addresses. It has no address search, so your agent needs latitude and longitude from somewhere else:
Tell the model which of these it has. An agent that knows it can't look up an address will ask the user for a location instead of inventing one.
A directions request carries where someone is and where they're going, which is sensitive. With a hosted map provider, that provider receives both every time your agent asks.
The Carpathian Maps server keeps coordinates to itself. It logs the method, path, status and time of each request and the id of the key that made it, never a body, a coordinate or a query string. Carpathian's platform sees only how many requests each app made each day, for usage and billing. The key stays on your backend, since a key in a browser or mobile app can be copied by anyone who opens it.
If you build turn-by-turn navigation on it, that navigation contributes anonymous traffic data, which carries no account, device, start or destination. The usage policy describes it, and your app's privacy notice should mention it.
Every error is a short JSON message, and an agent should treat each one differently.
422 No route was found. The engine found no route, often because a point is outside the areas directions cover. Return the message to the model so it can tell the user.429 means the key is past its requests a minute (retry after the Retry-After seconds) or its requests a day (resets at midnight UTC).503 means the routing engine is busy or rebuilding. Retry later instead of letting the model guess.Each organization gets a free monthly allowance of route and travel-time requests, then they're billed per 1,000 at the rate on the Carpathian Maps service page. Requests the server fails on with a 5xx don't count, though a 422 No route was found. does, since the engine did the work. Each app has its own requests a minute and a day, so a looping agent can't run up a bill.
The limits to plan around:
Enable Carpathian Maps from Services in your Carpathian account, create an app, and its key drops straight into the functions above. The API reference lists every field and error, and https://maps.carpathian.ai/llms.txt gives your coding assistant the docs. For an agent that also searches the web, How to Let AI Search the Web for Free adds the other half, and the AI API runs the model itself in the OpenAI format.