Skip to content

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.

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 use integrity. 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.js serves the release marked latest in versions.json: currently 0.8.0, not 0.10.0. When latest moves, every page that loads wander-embed.js moves with it within minutes (the file is cached for five minutes). Don’t put an integrity hash 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 names latest and the wire protocol. 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.

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}.

OptionDefaultDescription
mapIdRequiredIdentifies the Wander map.
originhttps://web.wander-app.comSets the embed origin.
namespacedefaultIsolates this embed’s messages and maps to the ns URL parameter.
pois[]Supplies an array of POI IDs.
daysNoneSupplies day numbers positionally aligned with pois.
stopsNoneSupplies the itinerary as [{ poiId, day }] in visit order; overrides pois and days. Repeated IDs are separate stops.
listIdNoneLoads a saved list and maps to the sb URL parameter.
routeNoneEnables full route mode.
routeTypedrivingSets driving, walking, or cycling routing when provided.
showNumbersfalseShows numbered marker badges when enabled.
showAllPoistrueShows other POIs from the map when enabled.
showControlstrueShows map controls when enabled.
controlsPositionbottom-rightPositions controls at a supported map corner.
zoomAuto-fitSets the initial zoom. Applied only alongside center — see the note below.
centerAuto-fitSets the initial center as { lng, lat }.
titleWander mapSets the iframe’s accessible title.
height420Sets the initial iframe height in pixels.
autoResizetrueApplies the embed’s recommended height when it changes.
allowGeolocationfalseAdds 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.

MemberDescription
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.
.frameReferences 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.

map.on('poiSelected', ({ poiId }) => {
console.log('Selected POI:', poiId);
});
EventPayloadDescription
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.
errorReserved — not currently emittedReserved 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.

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'
});
OptionDefaultDescription
mapIdRequiredThe map’s ID or slug.
namespacedefaultIsolates this map’s messages; maps to the ns URL parameter.
originhttps://web.wander-app.comThe embed origin.
pathsiteembedThe 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.
modeemphasizeInitial display mode: only, emphasize or all. A plain iframe without the SDK defaults to all.
contextNoneInitial places. Only their IDs reach the embed, each as mentioned with no badge and no fit; call setContext for roles, badges and fitting.
paddingNoneCamera padding { top, right, bottom, left }, sent as the first command once the map is ready. All four sides are required.
showControlstruefalse hides the zoom, compass, geolocate and recenter controls.
showSearchtruefalse hides the search bar, and with it the embed’s search analytics.
associationsInCardtruefalse hides the related-place pills in the place card, for hosts that list related places themselves.
controlsPositionbottom-righttop-left, top-right, bottom-left or bottom-right.
fitUnsetboundary or point: how the map opens. Unset, venue maps fit their boundary.
centerUnset{ lng, lat } for the initial camera. Overrides fit. A latitude or longitude of 0 counts as unset, as does a zoom of 0.
zoomUnsetInitial zoom. Sent only together with center.
backgroundUnsetHex 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.
titleWander mapThe iframe’s accessible title.
height420Iframe height in pixels. Ignored with fill.
fillfalsePositions the iframe absolute, filling your container (which must be positioned), and never lazy-loads it.
lazytrueSets loading="lazy" on the iframe. Pass false for maps above the fold.
allowGeolocationfalseAdds 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.

MemberDescription
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').
.frameThe 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 ready are queued and sent in order once the embed is ready.
  • For site maps, ready is 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 answered ready. After setContext, the contextApplied event is the reliable sign that the embed understood it.
  • The SDK doesn’t notice if you reload .frame yourself: it stays connected, and commands sent while the new page loads are lost. Destroy the map and create a new one instead.

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.

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

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.

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: setLines and clearLines on linesApplied, animateLine on lineAnimationEnded for that line, setTerrain and resetTerrain on terrainChanged, setAppearance and clearAppearance on appearanceChanged, and setLineProgress on its ack. The value is that event’s payload.
  • When a later call replaces a running one, the earlier promise resolves too: an animateLine with cancelled: true, and a terrain, appearance or setLines call with the later call’s result.
  • It rejects with an Error whose code is the embed’s reason (such as unknown_line or no_terrain, with the error payload as detail) when the embed reports an error for that command; with code: 'timeout' when no answer arrives within timeoutMs (default 8000, counted from when the command is sent, so a call made before ready doesn’t time out while the map loads); and with code: 'destroyed' after destroy().
  • Against an embed that doesn’t list the feature in ready.capabilities, such as site embed 2.0.0, it rejects at once with code: 'capability_unavailable' (detail.capability names the feature) and sends nothing. A call made before ready waits for ready to 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.

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.

OptionDefaultDescription
mapIdRequiredIdentifies the Wander map.
height1100Sets the iframe height in pixels.
titleEvery way here, door to door — Wander Trip RaceSets the iframe’s accessible title.
originhttps://web.wander-app.comSets 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”.