Agents
An AI agent that needs a Canadian address, a coordinate, or a route has the same problem a developer has on their first day: it has to find out that unmap exists, learn what unmap can do, learn how to get a credential, and then make a call. A developer solves that by reading. An agent cannot read, so unmap publishes those answers as documents built for machines, and exposes one endpoint an agent can call directly.
This page covers (1) the documents an agent reads to discover unmap, (2) the A2A Agent Card and what it promises, (3) the eight skills the card advertises and how to invoke each one, (4) the errors the endpoint returns and what they mean, and (5) the tools the site registers for an agent working inside a browser. If you are writing ordinary application code rather than an agent, Calling the API directly is the page you want; everything here is built on the endpoints described there.
Discovery
Six unauthenticated documents describe unmap to a machine. Most are served from both
https://unmap.dev and https://api.unmap.dev, so an agent that starts from either host finds
the rest.
/.well-known/agent-card.jsonis the A2A Agent Card: unmap's identity, its skills, its security schemes, and the address of the endpoint that runs them. This is the entry point for an agent framework. Both hosts serve the same bytes./openapi.jsonis an OpenAPI 3.1 description of every REST endpoint, onapi.unmap.devonly. This is the entry point for a code generator or a tool-calling agent that prefers plain HTTP./.well-known/api-catalogis an RFC 9727 linkset that names the API and points at both of the above./.well-known/oauth-protected-resourceis RFC 9728 metadata: what this resource is and how a bearer credential is presented. It names no authorization server, because unmap runs none, and every401points at it./auth.mdexplains where a credential comes from, in prose. unmap runs no OAuth authorization server and offers no agent self-registration: a key is created by a human in the dashboard and handed to the agent. It is served fromapi.unmap.dev, andunmap.dev/auth.mdredirects there. See Authentication./.well-known/agent-skills/index.json, onunmap.dev, indexes four Markdown skills that teach an agent to call the plain HTTPS API, including the map-rendering half the endpoint below does not cover.
The MCP server publishes three more of its own, described on MCP server.
Every page of these docs is also available as Markdown. Ask for it with an Accept header and
you get the source rather than the rendered page:
curl -H "Accept: text/markdown" https://unmap.dev/docs/agents/a2aThe Agent Card
The card is A2A v1.0 and lives at the well-known path the specification names:
curl https://unmap.dev/.well-known/agent-card.json{
"name": "unmap",
"description": "Canadian geocoding and routing as an agent-callable service. …",
"supportedInterfaces": [
{ "url": "https://api.unmap.dev/a2a", "protocolBinding": "JSONRPC", "protocolVersion": "1.0" }
],
"version": "2.0.0",
"capabilities": { "streaming": false, "pushNotifications": false, "extendedAgentCard": false },
"skills": [ /* five, described below */ ]
}The card declares one interface, and that interface is a real endpoint. supportedInterfaces[0].url
is where an A2A client sends SendMessage, and https://api.unmap.dev/a2a answers it. This is
worth stating plainly because a card is a promise rather than a description: publishing one that
points at machinery that does not exist would waste the time of every client that believed it.
Three capabilities are false, each for a reason:
streaming: a geocode or a route is one round trip that finishes in milliseconds. There is nothing to stream.pushNotifications: unmap creates no A2ATask. Every skill answers synchronously with aMessage, so there is no asynchronous state for a webhook to report. The task methods (GetTask,CancelTask) answer-32001 Task not found, which is true: there are no tasks.extendedAgentCard: an authenticated client sees exactly this card. The OpenAPI document, these docs, and the pricing are all public, so a second, richer card would have nothing extra in it.
Authentication
The endpoint takes the same API key as the rest of the API, in a header:
Authorization: Bearer um_live_...
X-API-Key: um_live_... works as well; the card lists both as alternatives. The third form the
REST API accepts, ?key=, is deliberately absent from the card. It exists because MapLibre
cannot attach headers to the requests it fires for tiles, and an A2A client has no such
constraint. A live key in a URL ends up in logs and referrers.
A call with no key is answered with HTTP 401 and a WWW-Authenticate challenge, not with a
JSON-RPC error body, so the challenge reaches the client that needs it. The same is true of
429 (rate limit or quota, carrying Retry-After) and 503 (a backend still starting). Get a
key at unmap.dev/dashboard/keys.
Calling a skill
The binding is JSON-RPC 2.0 over HTTPS, and the method name is SendMessage. A skill is selected
by a data part, an object in message.parts carrying a skill field and that skill's
parameters:
curl -X POST https://api.unmap.dev/a2a \
-H "Authorization: Bearer $UNMAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"messageId": "1f0c…",
"role": "ROLE_USER",
"parts": [{ "data": { "skill": "geocode_search", "query": "17 Ave SW, Calgary", "limit": 3 } }]
}
}
}'The reply is a Message with two parts: a text part summarising the answer in a form a model can
read without parsing anything, and a data part holding the API's own JSON, untouched. The summary
is written in English whatever language you ask for; only the place names follow language.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"message": {
"messageId": "…",
"contextId": "…",
"role": "ROLE_AGENT",
"parts": [
{ "text": "3 results:\n17 Avenue SW [street] at -114.07…", "mediaType": "text/plain" },
{ "data": [ /* the same array /geocode/search returns */ ], "mediaType": "application/json" }
]
}
}
}Coordinates are always [longitude, latitude] in WGS84 degrees, in that order. The "lng,lat"
string form is accepted too, so [-114.0625, 51.0447] and "-114.0625,51.0447" mean the same
thing.
The same skills over MCP
These are the same eight skills the MCP server exposes as
tools, running the same code: one table in the gateway feeds this card, the MCP tools/list
response, and the OpenAPI description. A skill behaves identically whichever envelope you reach it
through, including its error messages, and the two surfaces cannot drift apart. If your framework
speaks MCP, prefer it; A2A is here for frameworks that speak A2A. There is no capability on one
that is missing from the other.
A message with only text
If a message carries no data part, its text is read as a geocoding query and nothing else:
{ "role": "ROLE_USER", "parts": [{ "text": "Iqaluit" }] }That is the one inference unmap makes, and it is defensible because free text is literally what the full-text geocoder takes. It is not an understanding of the sentence. unmap runs no language model, so "how do I drive to Banff" is searched for as a place name rather than routed, and the reply says so in its text part. To reach the other seven skills, send a data part.
The eight skills
| Skill | Required | Optional |
|---|---|---|
geocode_search | query | language, limit, bbox |
geocode_reverse | coordinate | language, limit |
geocode_nearby | categories, near | radius, limit, language |
route | from, to | mode, truck dimensions |
isochrone | center, minutes | mode, truck dimensions |
identify | coordinate | layers, geometry |
data_query | layer, bbox | limit, geometry |
list_layers | addon |
identify answers which catalog polygons contain a point, which is a different question from
geocode_reverse's nearest named places. data_query takes one layer at a time. list_layers
needs no arguments and reads the catalog, including each layer's licence and known gaps: worth
calling before telling a user a layer covers them, since several cover one province.
coordinate, near, from, to and center are all [longitude, latitude] pairs.
categories and minutes are arrays. mode is one of auto, car, bicycle, pedestrian
or truck, and the truck dimensions (height, width, length, weight, axle_load,
axle_count, hazmat, use_truck_route) apply only when mode is truck. The full JSON
Schema for every skill comes back from the MCP server's tools/list, on a live connection;
/openapi.json names the skills and the envelope around
them. The underlying behaviour is documented on the Geocoding API and
Routing API pages.
A few notes on what is and is not here:
- Autocomplete is not a skill. It exists to rank a half-typed prefix for a person watching a
dropdown. An agent has the whole string already, and
geocode_searchis the endpoint tuned to return the best answer for one. - Tiles and styles are not skills. A vector tile and a MapLibre style document are things a renderer consumes, and neither survives the trip through an A2A message. The only useful thing to hand back would be a style URL with your API key embedded in it, which is a credential to leak rather than a skill to call. If your agent needs a map, build the URL from Maps API.
An A2A call is metered as one request, billed to the service the skill actually used: a
geocode_search counts as a geocode, exactly like GET /geocode/search and exactly like the MCP
tool. A call that runs no skill still costs one request. See
Plans and limits.
Errors
There are two kinds of failure, and they arrive differently on purpose.
A skill that fails comes back as a successful result. The Message carries the failure in its
text part and sets metadata.isError. That is deliberate: a JSON-RPC error is invisible to a
model, so it cannot read it and try something else, while a result it can read is something it can
act on. "No route between those points", "unknown category", "to must be [longitude, latitude]"
and "geocoding is not available on this deployment" all arrive this way, with the same wording
they have over MCP.
{
"jsonrpc": "2.0", "id": 1,
"result": { "message": {
"role": "ROLE_AGENT",
"parts": [{ "text": "to must be [longitude, latitude]", "mediaType": "text/plain" }],
"metadata": { "isError": true }
}}
}A malformed request is a JSON-RPC error, with HTTP 200, as the specification requires.
Transport failures arrive as themselves.
| Code | Meaning |
|---|---|
-32700 | The body was not valid JSON. |
-32600 | Not a single JSON-RPC 2.0 request object. Batches are refused. |
-32601 | Unknown method. unmap implements SendMessage. |
-32602 | The envelope could not be read as a skill call: no skill named, an unknown skill, or no usable part. |
-32001 | Task not found. unmap creates no tasks. |
-32003 | Push notifications are not supported. |
-32004 | Streaming is not supported. |
-32007 | No extended agent card is configured. |
HTTP 401 | Missing or rejected key, with a WWW-Authenticate challenge. |
HTTP 429 | Rate limit, quota, or spend cap, with Retry-After where a wait helps. |
HTTP 503 | A backend is still starting. Retryable. |
Authentication, rate limiting and quotas are answered at the transport layer rather than inside a
200, because a challenge buried in a JSON-RPC body is one your client cannot act on.
In the browser
Everything above assumes an agent making HTTP requests of its own. An agent driving a browser (a browser extension, or an assistant working in the tab a person is looking at) has a second option: unmap.dev registers WebMCP tools with the browser's model context on every page, so the site can be used rather than scraped.
| tool | what it does |
|---|---|
search_unmap_docs | Full-text search across these docs. Returns ranked sections with their URLs and a matching excerpt. |
read_unmap_page | Any page of unmap.dev as Markdown, the same bytes the Accept header above returns. |
navigate_unmap_site | Opens a page of the site in the current tab. |
list_unmap_map_styles | The twelve basemap styles in light and dark, with the flavor value that requests each one. |
None of them needs a key, and none of them calls the API: search runs against a static index in the browser, and the other three read pages this site already serves. Anything that costs a call (a geocode, a route, a tile) is deliberately absent, and that is what the Agent Card and the REST endpoints above are for.
WebMCP is a draft and the API has been in two places: Chrome's early preview exposes it on
navigator, while the specification now defines it on Document. unmap registers on whichever
the browser has. A browser with neither is unaffected: nothing is polyfilled, and no global is
patched.
WebMCP is the reference for these four: each tool's arguments, what it returns, and what it refuses.
Coverage
unmap covers Canada. The geocoding corpus is Canadian addresses and places, and the routing graph stops at the border, so a route that crosses into the United States has no answer rather than a partial one. Names are carried in English, French, and, where the source data has them, Indigenous languages on the same record, and a query written in syllabics matches. Coverage of Indigenous names is a sourcing gap rather than an engine one: the corpus carries an Inuktitut name for relatively few communities.
Next steps
- MCP server is the same capabilities over the Model Context Protocol.
- API reference for every endpoint an agent can reach.
- Errors for what a failed tool call returns.