Skip to content

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.json is 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.json is an OpenAPI 3.1 description of every REST endpoint, on api.unmap.dev only. This is the entry point for a code generator or a tool-calling agent that prefers plain HTTP.
  • /.well-known/api-catalog is an RFC 9727 linkset that names the API and points at both of the above.
  • /.well-known/oauth-protected-resource is RFC 9728 metadata: what this resource is and how a bearer credential is presented. It names no authorization server, because unmap runs none, and every 401 points at it.
  • /auth.md explains 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 from api.unmap.dev, and unmap.dev/auth.md redirects there. See Authentication.
  • /.well-known/agent-skills/index.json, on unmap.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/a2a

The 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 A2A Task. Every skill answers synchronously with a Message, 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

SkillRequiredOptional
geocode_searchquerylanguage, limit, bbox
geocode_reversecoordinatelanguage, limit
geocode_nearbycategories, nearradius, limit, language
routefrom, tomode, truck dimensions
isochronecenter, minutesmode, truck dimensions
identifycoordinatelayers, geometry
data_querylayer, bboxlimit, geometry
list_layersaddon

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_search is 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.

CodeMeaning
-32700The body was not valid JSON.
-32600Not a single JSON-RPC 2.0 request object. Batches are refused.
-32601Unknown method. unmap implements SendMessage.
-32602The envelope could not be read as a skill call: no skill named, an unknown skill, or no usable part.
-32001Task not found. unmap creates no tasks.
-32003Push notifications are not supported.
-32004Streaming is not supported.
-32007No extended agent card is configured.
HTTP 401Missing or rejected key, with a WWW-Authenticate challenge.
HTTP 429Rate limit, quota, or spend cap, with Retry-After where a wait helps.
HTTP 503A 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.

toolwhat it does
search_unmap_docsFull-text search across these docs. Returns ranked sections with their URLs and a matching excerpt.
read_unmap_pageAny page of unmap.dev as Markdown, the same bytes the Accept header above returns.
navigate_unmap_siteOpens a page of the site in the current tab.
list_unmap_map_stylesThe 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.