Embed SDK
wander-embed.js creates a list embed, a site embed, or a getting-here widget and gives your page a JavaScript API for controlling it and receiving events.
Pinning a version
Section titled “Pinning a version”For a production site, load a versioned file rather than the moving wander-embed.js, and add its integrity hash so the browser refuses a file that doesn’t match. This is the newest release, 0.10.0, which the site map’s story-map features need (0.9.0 and later have path sections):
<script src="https://developer.wandermaps.com/sdk/wander-embed-0.10.0.js" integrity="sha384-60kCl7EeS2FmqBQdS/KZGdq8gUObbt/1uZEhGOLto0A5e+7/o5b4XoQLZA7emRXw" crossorigin="anonymous"></script>- Keep
crossorigin="anonymous"whenever you useintegrity. The docs site sends the CORS header the browser needs to check the hash. - Versioned files are never modified after release. You can also copy one into your own build and self-host it; nothing in the SDK depends on being served from this domain.
- The unversioned
wander-embed.jsserves the release markedlatestinversions.json: currently 0.8.0, not 0.10.0. Whenlatestmoves, every page that loadswander-embed.jsmoves with it within minutes (the file is cached for five minutes). Don’t put anintegrityhash on it, because its bytes change when it moves. - The releases, with their hashes, are listed in the changelog and in
https://developer.wandermaps.com/sdk/versions.json, which also nameslatestand the wireprotocol. Releases before 0.6.0 have no hash.
At runtime, Wander.VERSION reports the SDK version and Wander.PROTOCOL the wire protocol (1). A site map also reports the embed’s own version: getEmbedVersion() returns ready.embedVersion ('2.1.0' for the current site embed), or null before ready. To find out what the embed can do, use hasCapability(name) rather than comparing versions. The SDK tells the embed its version through the sdk= URL parameter, and the embed echoes it back as ready.sdkVersion. If your page checks versions, compare each number separately: parseFloat('0.10.0') is 0.1.
If the SDK is unavailable for any reason, the iframe embeds work without it, so a host can fall back to a plain <iframe> with the same mapId and parameters.
Initialize a map
Section titled “Initialize a map”After loading the preview SDK, pass a selector or DOM element and a mapId:
<div id="wander-map"></div><script> const map = Wander.map('#wander-map', { mapId: 'wake-county', pois: [ 'f56b236a-1bb8-4afe-a302-d84b564a4cf6', '99f4dce3-dcd6-40aa-a297-6411cb8a7fd9' ], namespace: 'weekend-guide', allowGeolocation: true });</script>The SDK targets https://web.wander-app.com/listembed/{mapId}.
Options
Section titled “Options”| Option | Default | Description |
|---|---|---|
mapId | Required | Identifies the Wander map. |
origin | https://web.wander-app.com | Sets the embed origin. |
namespace | default | Isolates this embed’s messages and maps to the ns URL parameter. |
pois | [] | Supplies an array of POI IDs. |
days | None | Supplies day numbers positionally aligned with pois. |
stops | None | Supplies the itinerary as [{ poiId, day }] in visit order; overrides pois and days. Repeated IDs are separate stops. |
listId | None | Loads a saved list and maps to the sb URL parameter. |
route | None | Enables full route mode. |
routeType | driving | Sets driving, walking, or cycling routing when provided. |
showNumbers | false | Shows numbered marker badges when enabled. |
showAllPois | true | Shows other POIs from the map when enabled. |
showControls | true | Shows map controls when enabled. |
controlsPosition | bottom-right | Positions controls at a supported map corner. |
zoom | Auto-fit | Sets the initial zoom. Applied only alongside center — see the note below. |
center | Auto-fit | Sets the initial center as { lng, lat }. |
title | Wander map | Sets the iframe’s accessible title. |
height | 420 | Sets the initial iframe height in pixels. |
autoResize | true | Applies the embed’s recommended height when it changes. |
allowGeolocation | false | Adds iframe geolocation permission when enabled; use true for accurate visitor analytics. |
An explicit camera in the URL suppresses the embed’s auto-fit, so a zoom with no center opens the map on its default centre with your POIs out of frame. The SDK therefore only sends camera parameters when center is present; to open on a particular place, pass both, or let the embed fit the POIs and call flyTo once it is ready.
Methods and properties
Section titled “Methods and properties”| Member | Description |
|---|---|
flyTo(options) | Moves the camera using poiId, center, zoom, bearing, pitch, or duration. Camera only — pass select: true to also open the POI’s detail popup. |
select(poiId) | Selects a POI. |
clearSelection() | Clears the selected POI. |
setActiveDay(day) | Shows one itinerary day; pass null to clear the day filter. |
fitBounds() | Fits the map to its current POIs. |
setPois(pois, days) | Replaces the POIs and optional aligned days, reloading the iframe. Accepts [{ poiId, day }] stops too, and returns immediately when the set is unchanged. |
setStops(stops) | Replaces the itinerary with [{ poiId, day }] stops. Alias of setPois. |
getItinerary() | Returns the most recent itineraryChanged payload, or null before one arrives. |
getActiveDay() | Returns the active day number, or null when every day is shown. |
getUrl() | Returns the embed URL the SDK is currently rendering. |
on(type, handler) | Subscribes to an SDK event. |
off(type, handler) | Unsubscribes a handler from an SDK event. |
isConnected() | Returns whether the iframe has completed its ready handshake. |
destroy() | Removes the iframe, listener, queued commands, and event handlers. |
.frame | References the generated iframe element. |
Commands sent before ready are queued and flushed after the iframe connects, so you do not need to guard against an initialization race.
Wander.embedUrl(options) builds the same URL without mounting anything, for pages that render their iframe server-side.
Events
Section titled “Events”map.on('poiSelected', ({ poiId }) => { console.log('Selected POI:', poiId);});| Event | Payload | Description |
|---|---|---|
ready | { namespace, mapId, poiCount, maxDay } | Fires once the map is ready to accept commands. It fires before the POI fetch resolves, so read the itinerary from itineraryChanged rather than from this payload. |
itineraryChanged | { poiCount, maxDay, poiIds, days } | Reports the itinerary the embed actually loaded, where days is [{ day, poiIds }]. Fires when the data lands and again whenever the set changes. An empty itinerary is never reported. |
dayChanged | { day, maxDay } | Reports the active day, null for all days. Fires for the embed’s own day chips as well as setActiveDay. |
dimensionChanged | { width, height } | Reports the embed’s recommended dimensions. |
poiSelected | { poiId } | Fires when a POI is selected. |
poiDeselected | {} | Fires when the current POI is deselected. |
boundsChanged | { center, zoom, bounds: { n, s, e, w } } | Reports a changed viewport. |
error | Reserved — not currently emitted | Reserved for future SDK errors. |
When autoResize is true, the SDK applies the height from dimensionChanged to the iframe automatically. Messages include the configured namespace, so multiple embeds can coexist on the same page without receiving one another’s commands or events.
ready, itineraryChanged, and dayChanged describe state rather than a moment, so a handler subscribed after one has already fired still receives the last payload. The other events are only delivered live.
Set autoResize: false when your own layout owns the map’s height — a drawer or flex container, for example — and size the iframe yourself through map.frame.style.
For end-to-end integrations built on these events, see Itineraries & listing maps.
Initialize a site map
Section titled “Initialize a site map”Wander.siteMap(target, options) mounts a site embed (/siteembed/{mapId}) whose context can change without reloading its iframe. The site embed page has the bridge-level detail: every command’s exact behaviour, every event payload, and how the map behaves.
const site = Wander.siteMap('#map', { mapId: 'YOUR-MAP-ID', namespace: 'my-site', mode: 'emphasize', padding: { top: 20, right: 20, bottom: 20, left: 620 }, associationsInCard: false, title: 'Map of the valley', fill: true, background: '#ECE5D6'});Site map options
Section titled “Site map options”| Option | Default | Description |
|---|---|---|
mapId | Required | The map’s ID or slug. |
namespace | default | Isolates this map’s messages; maps to the ns URL parameter. |
origin | https://web.wander-app.com | The embed origin. |
path | siteembed | The route segment. Only lowercase letters, digits and hyphens are allowed; anything else throws. Keep the default: siteembed-next is a temporary alias of the same embed and will be removed. |
mode | emphasize | Initial display mode: only, emphasize or all. A plain iframe without the SDK defaults to all. |
context | None | Initial places. Only their IDs reach the embed, each as mentioned with no badge and no fit; call setContext for roles, badges and fitting. |
padding | None | Camera padding { top, right, bottom, left }, sent as the first command once the map is ready. All four sides are required. |
showControls | true | false hides the zoom, compass, geolocate and recenter controls. |
showSearch | true | false hides the search bar, and with it the embed’s search analytics. |
associationsInCard | true | false hides the related-place pills in the place card, for hosts that list related places themselves. |
controlsPosition | bottom-right | top-left, top-right, bottom-left or bottom-right. |
fit | Unset | boundary or point: how the map opens. Unset, venue maps fit their boundary. |
center | Unset | { lng, lat } for the initial camera. Overrides fit. A latitude or longitude of 0 counts as unset, as does a zoom of 0. |
zoom | Unset | Initial zoom. Sent only together with center. |
background | Unset | Hex colour (#RGB or #RRGGBB). Painted on the iframe element and sent to the embed as bg, so your page colour shows until the first tiles paint. |
title | Wander map | The iframe’s accessible title. |
height | 420 | Iframe height in pixels. Ignored with fill. |
fill | false | Positions the iframe absolute, filling your container (which must be positioned), and never lazy-loads it. |
lazy | true | Sets loading="lazy" on the iframe. Pass false for maps above the fold. |
allowGeolocation | false | Adds allow="geolocation" to the iframe. |
The SDK also adds sdk=<its version> to the URL. Wander.siteEmbedUrl(options) returns the same URL, with the same defaults, for a server-rendered iframe.
Site map methods
Section titled “Site map methods”| Member | Description |
|---|---|
setContext(items, { mode, fit, padding, reveal }) | Replaces the page context without changing frame.src. Each item is { poiId, role, badge?, geometry?, accent?, accentColor? }, and role (featured or mentioned) is required: an item without one makes the embed ignore the whole call. padding may give some sides only; they are kept for later calls. accent: 'glow', accentColor (#RRGGBB) and reveal: { stagger } need embed 2.1.0 (see Pin accent and reveal); the SDK throws on any other accent, a malformed accentColor or a reveal without a numeric stagger. (0.10.0) |
setMode(mode) | Changes the display mode and resets the page as setContext does. |
setPadding({ top, right, bottom, left }) | Sets and keeps the camera padding. All four sides are required. |
hover(poiId | null) | Shows hover for a place in the context or among the loaded places. Pass null to clear; a call with no argument is ignored. |
highlight(poiId | null) | Highlights an association target of the current anchors and fits it with its anchor. null clears. |
fitBounds(poiIds?, padding?) | Fits the listed places, or the current context. Also accepts one object, fitBounds({ poiIds, padding }). |
flyTo({ poiId, center, zoom, bearing, pitch, duration, select }) | Moves the camera. duration is in milliseconds (default 900, at most 5000); 0 jumps instantly. select: true also opens poiId. |
select(poiId, { fitAssociations }) | Opens a place. With nothing focused it becomes the anchor; otherwise it is a peek. |
focus(poiId | poiIds, { fit }) | Makes one place, or a set, the anchor and pushes it onto the focus stack. Throws when given neither a string nor a non-empty array. |
back() | Returns to the previous anchor set from cache. |
clearFocus() | Empties the focus stack and clears the selection. |
clearSelection() | Closes a peek and its anchor; the focus stack is kept. |
getAssociations(poiId, { timeoutMs }) | Returns a promise for { poiId, items }. It rejects when no reply arrives within timeoutMs (default 8000), which is what a failed fetch produces, and when the map is destroyed. An ID without associations resolves to items: []. |
storyRoute(stops, { profile, color }) | Loads an itinerary route through [lng, lat] stops. Answers with storyRouteLoaded. (0.8.0) |
storyRouteProgress(at) | Draws the route up to at (legs completed plus the fraction of the current leg). Throws when at isn’t a finite number. (0.8.0) |
clearStoryRoute() | Removes the route. (0.8.0) |
setLines(lines, { fit, timeoutMs }) | Replaces the lines drawn on the map. Resolves with the linesApplied payload. See Lines. (0.10.0) |
clearLines({ timeoutMs }) | Removes every line. Sends setLines { lines: [] }. (0.10.0) |
animateLine(lineId, { to, from, duration, easing, timeoutMs }) | Draws a line on (or back) over time. Resolves with its lineAnimationEnded payload, cancelled: true when a later command replaced it. (0.10.0) |
setLineProgress(lineId, at, { timeoutMs }) | Sets how much of a line is drawn, with no animation, for scroll-driven drawing. Resolves on its ack. (0.10.0) |
setTerrain({ exaggeration, duration, timeoutMs }) | Animates the terrain relief to exaggeration. Resolves with terrainChanged. See Terrain. (0.10.0) |
resetTerrain({ duration, timeoutMs }) | Animates back to the map style’s own relief. Resolves with terrainChanged { source: 'style' }. (0.10.0) |
setAppearance({ dim, color, duration, timeoutMs }) | Dims the base map; your places stay bright. Resolves with appearanceChanged. See Appearance. (0.10.0) |
clearAppearance({ duration, timeoutMs }) | Removes the dim. Sends setAppearance { dim: 0 }. (0.10.0) |
getContext() | Returns the items last passed to setContext (or the context option). |
getFocus() | Returns the last focusChanged payload, or null. |
getEmbedVersion() | Returns the embed’s version from ready ('2.1.0'), or null before ready or from an embed that doesn’t report one. (0.8.0) |
hasCapability(name) | Returns true when the embed listed name in ready.capabilities: 'lines', 'terrain', 'appearance', 'accent' or 'reveal'. false before ready, for any other name, and for an embed that lists none (2.0.0). (0.10.0) |
isConnected() | Returns whether the embed has answered ready. |
on(type, handler) / off(type, handler) | Subscribes or unsubscribes. Both throw on an event name this SDK version doesn’t know. |
destroy() | Removes the iframe, the message listener, queued commands and handlers, and rejects pending getAssociations promises and pending 0.10.0 promises (code: 'destroyed'). |
.frame | The iframe element. You may restyle it. |
The path-section methods setSections, highlightSection and clearSections need 0.9.0 and are described in Path sections. The story-map methods need 0.10.0 and embed 2.1.0; see Story-map promises below.
- Commands issued before
readyare queued and sent in order once the embed is ready. - For site maps,
readyis not replayed to a handler subscribed after it fired, so subscribe in the same task that creates the map. isConnected()only shows that the embed answeredready. AftersetContext, thecontextAppliedevent is the reliable sign that the embed understood it.- The SDK doesn’t notice if you reload
.frameyourself: it stays connected, and commands sent while the new page loads are lost. Destroy the map and create a new one instead.
Site map events
Section titled “Site map events”ready, loading, ack, contextApplied, poiSelected, poiDeselected, poiHovered, poiUnhovered, clusterClicked, focusChanged, associationsLoaded, associations, associationSelected, associationHighlighted, associationTypeSelected, associationTypeCleared, storyRouteLoaded, searchPerformed, searchResultSelected, externalPlaceSelected, externalPlaceCleared, boundsChanged, dimensionChanged and error. Their payloads are in the site embed’s event table. ready carries embedVersion, sdkVersion and, from embed 2.1.0, capabilities; poiSelected and poiDeselected carry source, 'host' when your command caused the change and 'user' for the visitor. The SDK 0.9.0 section events are described in Path sections. SDK 0.10.0 adds linesApplied, lineAnimationEnded, terrainChanged and appearanceChanged, described in Story maps.
Full-bleed host
Section titled “Full-bleed host”const site = Wander.siteMap(el, { mapId, fill: true, background: '#ECE5D6' });site.on('loading', () => showSkeleton());site.on('ready', () => hideSkeleton());site.on('poiSelected', ({ source }) => { if (source === 'user') closeSheet(); });ready can take 30–60 seconds on a satellite style over a slow connection. There is no failure event, so give it a timeout of your own.
Associations — anchor and peek
Section titled “Associations — anchor and peek”Associations are typed links between places: parking, trailhead, insider tip, food nearby, historically connected. The anchor set, usually the one place a page is about, sits on a focus stack. While a place is selected, the map draws the anchors’ associations by decorating each target’s own pin, with dotted connectors for access links. A first tap on a target, on the map or in the card, highlights it (associationHighlighted). A second tap, or the row’s Details ›, peeks at it: its card opens, the anchor’s associations stay, and the card offers Focus here (a new anchor, pushing the old one onto the stack) and Back. Opening any card while nothing is focused makes that place the anchor, so a visitor’s first click always shows associations.
const site = Wander.siteMap(el, { mapId, mode: 'emphasize' });site.on('focusChanged', ({ poiIds, depth, reason }) => renderCrumbs(poiIds, depth));site.on('associationSelected', ({ toPoiId }) => showPeekBar(toPoiId));site.setContext([{ poiId, role: 'featured' }]);site.focus(poiId, { fit: true });site.getAssociations(poiId).then(({ items }) => renderStrip(items));site.back();Use emphasize rather than only on pages that show associations, so the rest of the destination stays visible.
Story routes
Section titled “Story routes”A scroll-driven itinerary sends its stops once, then streams progress and the camera as the visitor scrolls. Only your progress moves the route.
site.on('storyRouteLoaded', ({ legs, fallback }) => renderLegCards(legs, fallback));site.storyRoute([[-77.86, 40.93], [-77.80, 40.95], [-77.75, 40.90]], { profile: 'driving', color: '#2F6B3A' });
// On every scroll frame: 3 stops make 2 legs, so `at` runs from 0 to 2.site.storyRouteProgress(at);site.flyTo({ center: { lng, lat }, zoom: 13, bearing, pitch: 50, duration: 0 });
// When the visitor leaves the itinerary:site.clearStoryRoute();site.flyTo({ center: { lng, lat }, pitch: 0, bearing: 0, duration: 0 });See Itinerary story routes for what is drawn and for the storyRouteLoaded legs.
Story-map promises
Section titled “Story-map promises”SDK 0.10.0 adds lines, runtime terrain and a dimmed base map for scroll-driven story pages. Every new method returns a promise instead of the map:
- It resolves on the command’s completion event:
setLinesandclearLinesonlinesApplied,animateLineonlineAnimationEndedfor that line,setTerrainandresetTerrainonterrainChanged,setAppearanceandclearAppearanceonappearanceChanged, andsetLineProgresson itsack. The value is that event’s payload. - When a later call replaces a running one, the earlier promise resolves too: an
animateLinewithcancelled: true, and a terrain, appearance orsetLinescall with the later call’s result. - It rejects with an
Errorwhosecodeis the embed’sreason(such asunknown_lineorno_terrain, with theerrorpayload asdetail) when the embed reports an error for that command; withcode: 'timeout'when no answer arrives withintimeoutMs(default 8000, counted from when the command is sent, so a call made beforereadydoesn’t time out while the map loads); and withcode: 'destroyed'afterdestroy(). - Against an embed that doesn’t list the feature in
ready.capabilities, such as site embed 2.0.0, it rejects at once withcode: 'capability_unavailable'(detail.capabilitynames the feature) and sends nothing. A call made beforereadywaits forreadyto decide. - A malformed argument throws at the call site, before anything is sent. The rules are in Story maps.
Check the capability first, or catch the rejection, so a page doesn’t log an unhandled rejection on an older embed:
const can = (name) => site.hasCapability(name);
if (can('lines')) { site.setLines([{ lineId: 'creek', pathId: 'PATH-ID', color: '#8FD0FF', glow: true, progress: 0 }]) .then(({ missing }) => { if (missing.length) console.warn('Lines not drawn', missing); return site.animateLine('creek', { to: 1, duration: 2600 }); }) .catch((error) => console.warn('Line not drawn:', error.code));}
// Cleanup that is safe on any embed:site.clearLines().catch(() => {});setContext stays chainable and never rejects: an embed without accent or reveal ignores those fields and applies the rest of the call.
A page opened from file:// has the opaque origin "null". The embed sends its acknowledgements to '*' in that case, so local demo pages work.
Getting here widget
Section titled “Getting here widget”The getting-here widget compares every door-to-door way to reach the destination, including driving and nonstop flights from nearby airports, with airport time counted in each option.
<div id="getting-here"></div><script src="https://developer.wandermaps.com/sdk/wander-embed.js"></script><script>Wander.gettingHere('#getting-here', { mapId: 'YOUR-MAP-ID' });</script>The SDK adds both the widget iframe and its required Wander attribution link to the container.
The widget’s Share button needs clipboard-write and web-share; without them, visitors get a copy-this-link box instead.
| Option | Default | Description |
|---|---|---|
mapId | Required | Identifies the Wander map. |
height | 1100 | Sets the iframe height in pixels. |
title | Every way here, door to door — Wander Trip Race | Sets the iframe’s accessible title. |
origin | https://web.wander-app.com | Sets the widget origin. |
For a server-rendered iframe, use Wander.gettingHereUrl() for src and include the same attribution link immediately after the iframe:
<iframe src={Wander.gettingHereUrl({ mapId: 'YOUR-MAP-ID' })} title="Every way here, door to door — Wander Trip Race" allow="geolocation; clipboard-write; web-share" loading="lazy" style={{ display: 'block', width: '100%', border: 0, borderRadius: '14px', height: '1100px' }}/><a href="https://www.wandermaps.com/?utm_source=dmo_embed&utm_medium=trip_race_sdk&utm_campaign=YOUR-MAP-ID" target="_blank" rel="noopener nofollow" style={{ display: 'inline-block', marginTop: '6px', font: '600 12px/1.4 system-ui, sans-serif', color: '#3A2D27', textDecoration: 'none' }}> Trip planner by Wander</a>The widget is only available for maps with a getting-here configuration. Other maps show “Getting here isn’t set up for this destination yet”.