RoveIQ Embed
Put an interactive RoveIQ wayfinding map inside your own web page, then drive it from your page’s code — change floors, highlight a store, draw walking directions — and react as visitors explore it. It’s an iframe plus a postMessage API: no SDK, no API key.
Quick start
A working integration is four steps. Everything after this section is detail on top of it.
1. Drop in the iframe
Point it at your venue. Your RoveIQ contact will give you the venue ID.
<iframe
id="roveiq"
src="https://embed.roveiq.com/venues/<venueId>"
title="Venue map"
allow="geolocation"
style="width: 100%; height: 600px; border: 0;"
></iframe> 2. Listen for messages
The map talks to your page with window.postMessage. Filter on source so you only see RoveIQ traffic.
window.addEventListener('message', (event) => {
// Ignore anything that isn't from the map.
if (event.data?.source !== 'roveiq-embed') return;
const { type, payload } = event.data;
console.log(type, payload);
}); 3. Wait for embed:ready
The map sends this once it has finished loading and can accept commands. It carries the venue’s floor list.
let floors = [];
window.addEventListener('message', (event) => {
if (event.data?.source !== 'roveiq-embed') return;
if (event.data.type === 'embed:ready') {
floors = event.data.payload.floors;
// Safe to send commands from here on.
}
}); 4. Send commands
Post back to the iframe with the matching host envelope. This helper is all you need.
const iframe = document.getElementById('roveiq');
function send(type, payload) {
iframe.contentWindow.postMessage({ source: 'roveiq-embed-host', type, payload }, '*');
}
send('map:highlightLocation', { locationId: 1234 });
send('map:showFloor', { floorId: floors[0].id }); Embedding the map
The URL
Every embed points at a single venue, identified by its numeric ID:
https://embed.roveiq.com/venues/<venueId> You can also deep-link straight to a location, so the map opens with that store already selected and its details panel showing:
https://embed.roveiq.com/venues/<venueId>/locations/<locationId> iframe attributes
| Attribute | Why |
|---|---|
allow="geolocation" | Required for blue-dot positioning and walking directions from the visitor’s current position. Omit it and those features stay off. The browser still asks the visitor for permission. |
title | Describes the frame for screen readers. Something like “Venue map” is fine. |
id | Any value — you need a handle on the element to send it commands. |
Sizing
The map fills whatever box you give it, so the iframe needs a real height — it will collapse if it only has a percentage height inside an auto-height parent. Two approaches that work:
/* Fixed height, full width. Simplest. */
#roveiq {
width: 100%;
height: 600px;
border: 0;
}
/* Responsive: hold an aspect ratio on desktop,
go taller on narrow screens so the map stays usable. */
#roveiq {
width: 100%;
aspect-ratio: 16 / 10;
border: 0;
}
@media (max-width: 700px) {
#roveiq {
aspect-ratio: auto;
height: 70vh;
}
} Sizing tip — The map has a mobile layout that engages below roughly 768 px of frame width, not window width. A narrow iframe on a wide desktop page will show the mobile interface — usually what you want, but worth knowing if a narrow sidebar embed looks unexpected.
Content Security Policy
If your site sends a CSP header, allow the embed origin as a frame source:
Content-Security-Policy: frame-src https://embed.roveiq.com; URL options
Append these as query parameters to tailor what the embed shows. All are optional, and all take 1 to switch on unless noted.
https://embed.roveiq.com/venues/<venueId>?hidesearch=1&zoom=19 Hiding interface
| Parameter | Effect |
|---|---|
hidesearch=1 | Hides the search bar. |
hideamenities=1 | Hides the amenities section (restrooms, ATMs, and similar). |
hidecontrols=1 | Hides all map controls and menus — zoom buttons, the floor changer, the side panel. Use this when your page provides its own interface and drives the map through the API. |
desktopDirections=0 | Turns off the directions interface on desktop, where it is on by default. |
Opening view
| Parameter | Range | Effect |
|---|---|---|
zoom | 0–22 | Starting zoom. 0 is fully out, 22 fully in. |
pitch | 0–85 | Camera tilt in degrees. 0 is straight down. |
bearing | 0–359 | Rotation in degrees. 0 is north, 90 east, 180 south, 270 west. |
Location services
| Parameter | Effect |
|---|---|
gpsEnabled=1 | Turns on positioning so visitors can see where they are and get directions from there. Needs allow="geolocation" on the iframe. |
enableDesktopGps=1 | Extends positioning to desktop, where it is off by default. |
Kiosks and touch screens
| Parameter | Effect |
|---|---|
disableInteraction=1 | Makes the map display-only. Panning, zooming and tapping are all switched off. |
emitclicks=1 | Posts a plain { type: 'resetInactivityTimerDataType' } message on every tap and keypress, so a kiosk shell can keep its screensaver from firing while someone is using the map. This one message does not use the envelope described below. |
Message envelope
Everything in both directions uses the same three-field shape, so you can tell RoveIQ traffic apart from any other postMessage activity on your page — analytics tags, chat widgets, other iframes.
// Map → your page
{ source: 'roveiq-embed', type: 'map:floorChange', payload: { … } }
// Your page → map
{ source: 'roveiq-embed-host', type: 'map:showFloor', payload: { … } } Always check source before reading a message. Anything without the matching value is ignored in both directions.
Send from the hosting page — Commands are only accepted from the window that embedded the iframe — that is,
iframe.contentWindow.postMessage(...)called from your own page. A message from anywhere else, such as another frame on the page or a popup, is rejected even with the correctsourcevalue. That string is a label, not a credential.
Startup and readiness
The map takes a moment to load its floor data and draw. Commands sent before it is ready fail, so wait for embed:ready.
| Who | What happens |
|---|---|
| Your page | Adds the iframe and starts listening. |
| Map | Loads venue data and renders. Silent throughout. |
| Map → | Sends embed:ready with the venue name, floor list and current floor. |
| Your page → | Sends commands. Receives map events as the visitor explores. |
If you start listening late
If your listener might attach after the map has already loaded — a component that mounts later, a tab that opens on demand — send embed:ping and the map replies with a fresh embed:ready.
send('embed:ping'); A ping that arrives while the map is still loading gets no reply. That is deliberate — you would rather have silence than an embed:ready that isn’t true yet. There is no need to retry or poll: the map sends embed:ready on its own the moment it finishes.
Events from the map
These arrive on your message listener as the visitor uses the map. You do not subscribe to anything — every embed sends all of them.
embed:ready
Map → your page. The map has finished loading and accepts commands. Sent once on load, and again whenever you send embed:ping.
| Field | Type | Notes |
|---|---|---|
venueId | number | The venue being shown. |
venueName | string | Display name of the venue. |
floors | { id, name, default }[] | Every floor, in display order. Use these IDs with map:showFloor. |
currentFloorId | number | The floor showing right now. |
commands | string[] | Every command this version of the embed accepts. Useful for feature-detection. |
map:pointerclick
Map → your page. Sent for every click or tap on the map, whether or not it landed on something. Read targetType first — it tells you which of the other fields are filled in.
| Field | Type | Notes |
|---|---|---|
targetType | string | location, pin, shape3d, or background when nothing was hit. |
locationId | number | For location and shape3d. |
location | { id, name } | When the clicked shape has a store attached. |
pinId | string | For pin. |
labelText | string | The label drawn on the shape, if any. |
floorId | number | Floor showing when the click happened. |
coordinates | [lng, lat] | Where the click landed. |
point | { x, y } | Screen position in CSS pixels, for positioning your own tooltip. |
if (type === 'map:pointerclick' && payload.targetType === 'location') {
openStoreDetails(payload.locationId, payload.location.name);
} map:hover
Map → your page. Sent when the shape under the pointer changes, including when the pointer moves onto empty space. Desktop only, and frequent — throttle any expensive work you hang off it.
| Field | Type | Notes |
|---|---|---|
hovering | boolean | false means the pointer left; no other fields are present. |
locationId | number | When the hovered shape has a store attached. |
location | { id, name } | Same. |
labelText | string | The shape’s label. May be empty. |
map:floorChange
Map → your page. The displayed floor changed — however it happened. This covers your own map:showFloor command, the visitor using the floor changer, and a route stepping between floors. If your page shows its own floor selector, follow this to keep the two in step.
| Field | Type | Notes |
|---|---|---|
id | number | Use with map:showFloor. |
name | string | The floor’s name. |
index | number | Display order. Lowest floor is 0. |
map:routeResult
Map → your page. The outcome of a map:route command. Exactly one of these arrives for every route you request, whether it worked or not — this is the message to use for “did my route command succeed”.
| Field | Type | Notes |
|---|---|---|
ok | boolean | Whether a route was found and drawn. |
to | string | The destination you asked for, as type\|id. |
from | string | Absent when the route started from the visitor’s position. |
legs | number | How many floor-to-floor segments. Only when ok. |
totalDistance | number | Walking distance in metres. Only when ok. |
if (type === 'map:routeResult') {
if (payload.ok) {
showDistance(Math.round(payload.totalDistance) + ' m walk');
} else {
showMessage("We couldn't find a walking route there.");
}
} map:routeFound
Map → your page. The shape of a route that was drawn, one entry per floor it crosses. Use this if you want the geometry — to draw your own overview, or to measure a leg.
| Field | Type | Notes |
|---|---|---|
legs | RouteLeg[] | One per floor segment. |
legs[].floorId | number | Floor this segment is on. |
legs[].floorName | string | That floor’s name. |
legs[].path | [lng, lat][] | The walking line for this segment. |
Not sent for every route — This one is best-effort: it is skipped for routes that specify a
from, and while live positioning is active. For a signal you can rely on for every request, usemap:routeResultabove.
map:noRoute
Map → your page. No walking route could be found to the requested destination. Sent alongside a map:routeResult with ok: false.
| Field | Type | Notes |
|---|---|---|
id | number \| string | The destination that could not be reached. |
map:arrivedFloorChange
Map → your page. During live navigation, the visitor has reached the point where the route changes floors — an escalator, a lift, a staircase. Same fields as map:floorChange, describing the floor they are heading to. Requires positioning to be switched on.
map:error
Map → your page. The map hit a problem. Check fatal first: when it is true the map has stopped drawing and needs to be reloaded.
| Field | Type | Notes |
|---|---|---|
fatal | boolean | The map is unusable and should be reloaded. |
code | string | Machine-readable cause. See below. |
message | string | Human-readable description, for your logs. |
context | object | Extra detail about what was happening. |
Current codes are webgl-context-lost, invalid-floor, asset-load-failed, render-failed and unknown. Treat this as an open list — handle unfamiliar values rather than matching on all of them exhaustively.
embed:commandError
Map → your page. A command you sent could not be carried out — an unknown floor ID, a missing locationId, or a command that arrived before the map was ready.
| Field | Type | Notes |
|---|---|---|
type | string | The command that failed. |
message | string | What went wrong. |
Commands to the map
Send these with the send() helper from the quick start. Anything that fails comes back as embed:commandError.
map:showFloor
Your page → map. Switches the displayed floor. Use an ID from the floors list in embed:ready.
send('map:showFloor', { floorId: 12 }); map:flyToLocation
Your page → map. Centres and zooms the map on a store, changing floors if it is on a different one. Does not highlight it — pair with map:highlightLocation if you want both.
send('map:flyToLocation', { locationId: 1234 }); map:highlightLocation
Your page → map. Draws a store in the venue’s highlight colour. Highlights persist until cleared, and you can have several at once.
send('map:highlightLocation', { locationId: 1234 }); map:clearHighlight
Your page → map. Removes one highlight, or all of them.
send('map:clearHighlight', { locationId: 1234 }); // just this one
send('map:clearHighlight', {}); // all of them map:route
Your page → map. Draws walking directions. Omit from to start at the visitor’s current position.
// Between two stores
send('map:route', {
from: { type: 'location', id: 1234 },
to: { type: 'location', id: 5678 }
});
// From wherever the visitor is standing
send('map:route', { to: { type: 'location', id: 5678 } });
// Step-free route to the nearest restroom
send('map:route', {
from: { type: 'location', id: 1234 },
to: { type: 'amenity', id: 12 },
handicap: true
}); | Field | Type | Notes |
|---|---|---|
to | RouteEndpoint | Required. See Route endpoints. |
from | RouteEndpoint | Omit to start from the visitor’s position. |
handicap | boolean | Step-free routing. Defaults to false. |
zoomToFit | boolean | Frame the whole route. Defaults to true. |
Routing from the visitor’s position — When you omit
from,tomust be alocation. Amenity and coordinate destinations need an explicitfrom— sent without one they are rejected with anembed:commandErrorrather than quietly routing somewhere wrong.
Routes drawn this way drive the map directly. They do not fill in the embed’s own directions panel or its turn-by-turn view.
map:clearRoute
Your page → map. Ends routing, erases the drawn route, and clears any highlights the route added. No payload.
send('map:clearRoute'); embed:ping
Your page → map. Asks the map to re-send embed:ready. No payload. See Startup and readiness.
Handling errors
Two messages report trouble, and they mean different things.
| Message | Means | What to do |
|---|---|---|
embed:commandError | Your command was rejected. The map is fine. | Check the message. Usually a bad ID or a command sent before embed:ready. |
map:error | The map itself hit a problem. | Log it. If fatal is true, reload the iframe. |
window.addEventListener('message', (event) => {
if (event.data?.source !== 'roveiq-embed') return;
const { type, payload } = event.data;
if (type === 'embed:commandError') {
console.warn('RoveIQ rejected', payload.type, payload.message);
}
if (type === 'map:error' && payload.fatal) {
// Reloading the frame rebuilds the map from scratch.
iframe.src = iframe.src;
}
}); Route endpoints
The from and to fields of map:route each take a route endpoint. Write it as an object, or as the equivalent type\|id string — they are interchangeable.
| Type | Object form | String form |
|---|---|---|
location | { type: 'location', id: 1234 } | 'location\|1234' |
amenity | { type: 'amenity', id: 12 } | 'amenity\|12' |
coordinate | { type: 'coordinate', id: '-84.51,39.10,12' } | 'coordinate\|-84.51,39.10,12' |
A coordinate is longitude,latitude,floorId — the floor matters, since the same point on the ground exists on every level. Any other type is rejected with an embed:commandError.
Common recipes
Your own store directory
Hide the built-in interface, list stores in your own markup, and drive the map from your clicks.
// Load with ?hidecontrols=1&hidesearch=1
document.querySelectorAll('.store').forEach((button) => {
button.addEventListener('click', () => {
const id = Number(button.dataset.locationId);
send('map:clearHighlight', {});
send('map:flyToLocation', { locationId: id });
send('map:highlightLocation', { locationId: id });
});
}); Keep your floor selector in step
Follow map:floorChange so your control tracks the map even when the visitor changes floors some other way.
const select = document.getElementById('floor-select');
select.addEventListener('change', () => {
send('map:showFloor', { floorId: Number(select.value) });
});
window.addEventListener('message', (event) => {
if (event.data?.source !== 'roveiq-embed') return;
const { type, payload } = event.data;
if (type === 'embed:ready') {
select.innerHTML = payload.floors
.map((f) => `<option value="${f.id}">${f.name}</option>`)
.join('');
select.value = payload.currentFloorId;
}
// Covers the floor changer, and routes that cross floors.
if (type === 'map:floorChange') {
select.value = payload.id;
}
}); “Take me there” button
function walkTo(locationId) {
send('map:route', { to: { type: 'location', id: locationId } });
}
// Report the outcome back to the visitor.
if (type === 'map:routeResult') {
status.textContent = payload.ok
? Math.round(payload.totalDistance) + ' m walk'
: 'No walking route available.';
} Limits and caveats
Worth knowing before you build
- Wait for
embed:ready. Commands sent earlier are rejected. There is no queue. - IDs come from RoveIQ. Venue, location and amenity IDs are issued by the RoveIQ portal. Your contact can supply the list for your venue, or you can read floor IDs out of
embed:ready. - Positioning needs permission. Anything involving the visitor’s location — routing without a
from,map:arrivedFloorChange— needsallow="geolocation"on the iframe, thegpsEnabled=1parameter, and the visitor accepting the browser prompt. - API-drawn routes stay on the map. They do not populate the embed’s own directions panel or turn-by-turn list.
map:hoveris desktop-only and fires often. Throttle anything expensive you attach to it.
Not currently reported
The map does not yet send events for zoom and pan, search activity, amenity toggles, or turn-by-turn navigation progress. If your integration needs any of these, tell your RoveIQ contact — they are straightforward to add.