# View links: agent answers on a map

A view link is a plan carried in a URL. An agent that plans a route, a day of
stops or a whole fleet hands back one link instead of a table; whoever opens it
sees the stops on a map at once, with each route redrawn by MapMap routing.

```
https://mapmap.ai/view?p=<payload>
```

No key, no account and no SDK are needed to open one, and nothing is stored on
our side: the link carries its whole plan. Open [/view](/view) without a
payload for the short version of this page.

## How an agent gets one

**From the MCP server.** The [MCP server](/docs/mcp) tools that answer with a
plan, `route`, `plan_day` and `optimise_routes`, return a `view_url` field
that holds a ready-made link. Pass it on as the last line of the answer. A
`view_url_note` comes with it when the link had to leave something out to fit
(arrival times, dwell times, then stop names), and in its place when a plan is
too big to link at all (more than six vehicles, more than 22 stops on a route,
or over the cap with names dropped). `route` links carry no stop names,
because a route request has none; `optimise_routes` links show arrival clocks
only when the request's time windows are seconds of a day, since any other
epoch has no clock to show. A self-hosted MCP server points its links at the
site in `SN_MCP_SITE_URL` (default `https://mapmap.ai`), and an empty value
turns them off.

**Built by hand.** The payload is base64url (RFC 4648 section 5, no padding) of
compact JSON in UTF-8:

```json
{"v":1,"kind":"route","costing":"auto","routes":[{"stops":[
  {"lat":51.5309,"lon":-0.1233,"name":"Kings Cross"},
  {"lat":51.5033,"lon":-0.1196,"name":"London Eye"}]}]}
```

```sh
P=$(printf '%s' "$JSON" | base64 | tr '+/' '-_' | tr -d '=\n')
echo "https://mapmap.ai/view?p=$P"
```

## What it carries

| Field | Rule |
|---|---|
| `v` | `1`. |
| `kind` | `route` (one route, the stops are its waypoints), `plan` (one route in visit order; a return to the start repeats the start as the last stop) or `fleet` (one route per vehicle). |
| `costing` | `auto`, `truck`, `bicycle`, `pedestrian` or `motor_scooter`. Absent draws as `auto`. |
| `truck` | With `costing: "truck"` only: `height_m`, `width_m`, `length_m`, `weight_t`, `hazmat`, `tunnel_code` (an ADR 8.6.4 code such as `C` or `B/D`). The redraw applies them, so a truck link is redrawn as that truck. |
| `depart_at` | ISO 8601 with an offset, shown as written. |
| `title` | At most 60 characters. |
| `routes` | `route` and `plan`: exactly 1. `fleet`: 1 to 6. Each has an optional `label` (at most 24 characters), 2 to 22 `stops`, and optional `totals`. |
| `stops[]` | `lat`, `lon` (required); `name` (at most 40 characters), `arrive` (`HH:MM`, 24-hour local clock) and `dwell_min` (whole minutes) optional. |
| `totals` | `distance_m` and `duration_s` as the agent planned them, for the whole plan or (on a route) for that vehicle. `duration_s` is travel time; dwell is not included. |

Unknown fields are ignored, so a newer link still opens on an older page. A
link that breaks a structural rule (the version, the kind, the number of routes
or stops, a coordinate) does not open; an optional field that breaks its rule is
left out, and a string over its limit is cut to it.

**What it does not carry:** no key, no email, no account, nothing about who made
it. The stop coordinates and names, the title and the route labels are the whole
of it, and anyone holding the link can read them. The query string is logged by
the web host like any URL, so treat a view link as you would the stop list
itself.

## What the page shows

- The stops at once, numbered per route, in each route's colour.
- Each route redrawn now with MapMap routing for the declared vehicle, and its
  distance and time as **redrawn now**.
- The agent's own totals as **as planned**, beside the redrawn figures and
  never in place of them. Routing is time-aware, so a redraw can differ from
  the moment the plan was made.
- The stop list with arrival clocks and dwell times when the link has them.
- A plain message when a route has no path for that vehicle, when the redraw
  is unavailable, or when the link cannot be read.

## The 6,000 character cap

The whole URL is at most 6,000 characters. A plan over the cap is trimmed in this order,
stopping as soon as it fits:

1. arrival clocks are dropped,
2. then dwell times,
3. then names are shortened to 20 characters,
4. then names are dropped.

A plan still over the cap gets no link; from the MCP server, `view_url_note`
says so instead. A six-vehicle fleet of 22 stops each fits once its names are
dropped.

## Writing an encoder

Any link that follows the rules above opens. If you want your links to match
MapMap's byte for byte (useful for caching and tests), encode canonically:

- Keys in this order: `v`, `kind`, `costing`, `truck` (`height_m`, `width_m`,
  `length_m`, `weight_t`, `hazmat`, `tunnel_code`), `depart_at`, `title`,
  `routes` (`label`, `stops`, `totals`), `totals`. A stop is `lat`, `lon`,
  `name`, `arrive`, `dwell_min`; totals are `distance_m`, `duration_s`. Leave
  absent fields out rather than writing `null`.
- Compact JSON, no whitespace outside strings; non-ASCII stays as UTF-8 and
  `/` is not escaped.
- Numbers as the shortest decimal that round-trips, integers without a fraction
  (`4`, not `4.0`), never an exponent, never `-0`.
- Coordinates rounded to 5 decimal places, half away from zero; `totals` and
  `dwell_min` rounded to whole numbers.
- Strings trimmed of surrounding whitespace; lengths count Unicode code points.

This plan:

```json
{
  "v": 1,
  "kind": "plan",
  "costing": "truck",
  "truck": { "height_m": 4, "width_m": 2.55, "length_m": 16.5, "weight_t": 40, "hazmat": true, "tunnel_code": "C" },
  "depart_at": "2026-10-20T08:00:00+01:00",
  "title": "Brighton delivery round",
  "routes": [
    {
      "label": "Van 1",
      "stops": [
        { "lat": 50.8289, "lon": -0.1411, "name": "Brighton station", "arrive": "08:00" },
        { "lat": 50.8277, "lon": -0.1697, "name": "Hove Town Hall", "arrive": "08:09", "dwell_min": 10 },
        { "lat": 50.8397, "lon": -0.1466, "name": "Preston Park Rotunda Café", "arrive": "08:29", "dwell_min": 15 }
      ]
    }
  ],
  "totals": { "distance_m": 6881, "duration_s": 1122 }
}
```

encodes to exactly this link ([open it](https://mapmap.ai/view?p=eyJ2IjoxLCJraW5kIjoicGxhbiIsImNvc3RpbmciOiJ0cnVjayIsInRydWNrIjp7ImhlaWdodF9tIjo0LCJ3aWR0aF9tIjoyLjU1LCJsZW5ndGhfbSI6MTYuNSwid2VpZ2h0X3QiOjQwLCJoYXptYXQiOnRydWUsInR1bm5lbF9jb2RlIjoiQyJ9LCJkZXBhcnRfYXQiOiIyMDI2LTEwLTIwVDA4OjAwOjAwKzAxOjAwIiwidGl0bGUiOiJCcmlnaHRvbiBkZWxpdmVyeSByb3VuZCIsInJvdXRlcyI6W3sibGFiZWwiOiJWYW4gMSIsInN0b3BzIjpbeyJsYXQiOjUwLjgyODksImxvbiI6LTAuMTQxMSwibmFtZSI6IkJyaWdodG9uIHN0YXRpb24iLCJhcnJpdmUiOiIwODowMCJ9LHsibGF0Ijo1MC44Mjc3LCJsb24iOi0wLjE2OTcsIm5hbWUiOiJIb3ZlIFRvd24gSGFsbCIsImFycml2ZSI6IjA4OjA5IiwiZHdlbGxfbWluIjoxMH0seyJsYXQiOjUwLjgzOTcsImxvbiI6LTAuMTQ2NiwibmFtZSI6IlByZXN0b24gUGFyayBSb3R1bmRhIENhZsOpIiwiYXJyaXZlIjoiMDg6MjkiLCJkd2VsbF9taW4iOjE1fV19XSwidG90YWxzIjp7ImRpc3RhbmNlX20iOjY4ODEsImR1cmF0aW9uX3MiOjExMjJ9fQ)):

```
https://mapmap.ai/view?p=eyJ2IjoxLCJraW5kIjoicGxhbiIsImNvc3RpbmciOiJ0cnVjayIsInRydWNrIjp7ImhlaWdodF9tIjo0LCJ3aWR0aF9tIjoyLjU1LCJsZW5ndGhfbSI6MTYuNSwid2VpZ2h0X3QiOjQwLCJoYXptYXQiOnRydWUsInR1bm5lbF9jb2RlIjoiQyJ9LCJkZXBhcnRfYXQiOiIyMDI2LTEwLTIwVDA4OjAwOjAwKzAxOjAwIiwidGl0bGUiOiJCcmlnaHRvbiBkZWxpdmVyeSByb3VuZCIsInJvdXRlcyI6W3sibGFiZWwiOiJWYW4gMSIsInN0b3BzIjpbeyJsYXQiOjUwLjgyODksImxvbiI6LTAuMTQxMSwibmFtZSI6IkJyaWdodG9uIHN0YXRpb24iLCJhcnJpdmUiOiIwODowMCJ9LHsibGF0Ijo1MC44Mjc3LCJsb24iOi0wLjE2OTcsIm5hbWUiOiJIb3ZlIFRvd24gSGFsbCIsImFycml2ZSI6IjA4OjA5IiwiZHdlbGxfbWluIjoxMH0seyJsYXQiOjUwLjgzOTcsImxvbiI6LTAuMTQ2NiwibmFtZSI6IlByZXN0b24gUGFyayBSb3R1bmRhIENhZsOpIiwiYXJyaXZlIjoiMDg6MjkiLCJkd2VsbF9taW4iOjE1fV19XSwidG90YWxzIjp7ImRpc3RhbmNlX20iOjY4ODEsImR1cmF0aW9uX3MiOjExMjJ9fQ
```
