A real globe, not a screenshot
The embed is the same map that runs on our terminal, served from https://www.solyzer.ai/embed/earth in a frame of its own. It fetches live data on its own schedule, it renders source credit that cannot be turned off, and it carries no cookie and no session of any kind.
Two ways in. The script tag watches your document and mounts a frame wherever it finds the container attribute, including containers your own JavaScript adds later. The plain iframe loads nothing from us but the frame itself, which is the right choice if your Content Security Policy is strict about scripts.
Embedding on your own site takes a publishable key. Keys are free, and publishable is meant literally: the key is designed to sit in your page where every visitor can read it, so the console shows it in full with a copy button whenever you need it, not only at the moment you create it. A keyless frame still renders on solyzer.ai and on localhost or 127.0.0.1 at any port, which is how you build one and try it before you sign up; on any other page it shows a line saying it needs a key instead of a map. The key also names your traffic, raises your ceiling, and locks the frame to the domains you registered.
Paste one of these
Every snippet below needs your own key
pk_earth_live_YOUR_KEY is a placeholder, not a key. Swap in a publishable key from the builder before this goes on your site. Keys are free, and the frame refuses one it does not recognize just as plainly as a missing one. There is nothing to catch and save: the builder lists your publishable keys in full, with a copy button, every time you open it.
While you are building on localhost or 127.0.0.1, at any port, you can delete the data-key line and the frame still renders. That allowance covers your own machine and our pages, nothing else: on any other origin a keyless frame is refused before a single upstream call, and says so in the box rather than going blank. Putting the key back is the last step before you ship, not the first step after somebody reports it broken.
Script tag
The loader at https://www.solyzer.ai/embed.js is a dependency free file with one global, window.SolyzerEmbed. It scans on execute, again on DOMContentLoaded, then watches the document, so a container rendered later still mounts. Including it twice is harmless.
<!-- Solyzer Digital Twin Earth -->
<div
data-solyzer-earth
data-lat="-6.2088"
data-lon="106.8456"
data-zoom="9"
data-layers="quakes,disasters,cyclones"
data-key="pk_earth_live_YOUR_KEY"
data-height="520"
></div>
<script src="https://www.solyzer.ai/embed.js" async></script>Plain iframe
No script from us at all. You lose the imperative API and the failure line the loader writes when a frame never reports ready; everything else is identical.
<iframe
src="https://www.solyzer.ai/embed/earth?lat=-6.2088&lon=106.8456&zoom=9&layers=quakes%2Cdisasters%2Ccyclones&k=pk_earth_live_YOUR_KEY"
title="Solyzer Digital Twin Earth"
width="100%"
height="520"
loading="lazy"
referrerpolicy="strict-origin-when-cross-origin"
allow="fullscreen"
sandbox="allow-scripts allow-same-origin allow-popups"
style="border:1px solid #26262a;border-radius:12px;display:block"
></iframe>React
The iframe form with React attribute names. There is no npm package to install and no component to keep in sync.
export function SolyzerEarth() {
return (
<iframe
src="https://www.solyzer.ai/embed/earth?lat=-6.2088&lon=106.8456&zoom=9&layers=quakes%2Cdisasters%2Ccyclones&k=pk_earth_live_YOUR_KEY"
title="Solyzer Digital Twin Earth"
loading="lazy"
referrerPolicy="strict-origin-when-cross-origin"
allow="fullscreen"
style={{ width: "100%", height: 520, border: 0, display: "block" }}
/>
);
}Build any of these against your own coordinates in the embed builder, which previews the real frame as you change it.
Every attribute
The script form reads data-* attributes on the container. The iframe form reads the query parameter in the second column. They are the same option; the loader maps one to the other generically, so the two lists can never disagree.
Swipe the table sideways for the rest of the columns.
| Attribute | Query param | Values | Default | What it does |
|---|---|---|---|---|
| data-latrequired | lat | number | none | Latitude to open on, -90 to 90. |
| data-lonrequired | lon | number | none | Longitude to open on, -180 to 180. |
| data-zoom | zoom | number | 9 | Opening zoom, 1 to 18. |
| data-layers | layers | list | quakes,disasters,cyclones | Comma list of layer ids. Unknown ids are named back to you in the frame, not silently dropped. |
| data-theme | theme | auto | dark | light | auto | dark or light is pinned inside the frame and nothing overrides it. auto, the default, means the frame follows your page: with the script tag the loader reads your site's own theme and keeps the frame in step with it, and in a plain iframe with no loader the frame follows the visitor's browser through prefers-color-scheme. If neither can be read it stays dark. |
| data-marker | marker | string | none | lat,lon,label for a single pin. |
| data-interactive | interactive | 0 | 1 | 1 | 0 makes the globe a still image: no drag, no zoom, no clicks. |
| data-scroll | scroll | off | on | off | Wheel zoom. Off by default so the embed never eats your page's scroll. |
| data-ui | ui | legend | sources | fullscreen | legend | Chrome inside the frame. The credit line always renders and cannot be removed, and a licences link beside it always opens the full per-source list, whatever you pass here. The list starts folded so it does not cover the map. |
| data-keyrequired | k | string | none | Your publishable key, pk_earth_live_... . Required to embed on your own site: a keyless frame renders only on solyzer.ai and on localhost or 127.0.0.1, so you can build and try one before you sign up, and everywhere else it is refused with a message instead of a map. Keys are free. It also names your traffic, raises the ceiling, and locks the embed to the domains you registered. It is public by design and lives in your page's source, so the console shows it in full with a copy button whenever you need it again, not only at the moment you create it. |
| data-height | loader only | number | 520 | Iframe height in px. Loader-only, never a query param. auto is accepted but has nothing to measure: the frame fills its box at 100dvh and has no intrinsic content height to report, so auto keeps the 520 fallback. Use data-aspect for responsive sizing. |
| data-aspect | loader only | string | none | Aspect ratio such as 16/9, used instead of a fixed height. The real answer for responsive sizing. Loader-only. |
Layer ids
data-layers takes a comma list of these ids. Data layers draw features; overlays change the ground or paint a raster on top of it. An id we do not recognize is named back to you inside the frame, never dropped in silence.
Data layers
quakes, disasters, air, radio, flights, iss, cameras, companies, exchanges, airports, countries, cities, seaports, rivers, launches, weather, cyclones, news
Overlays
overlay:satellite, overlay:roads, overlay:places, overlay:towers, overlay:traffic, overlay:radar, overlay:photos, overlay:satellites
Natural event categories also arrive with an ev: prefix, for example ev:wildfires. Those cannot be listed exhaustively here because the upstream category set changes, so any ev: id is accepted and the frame tells you if it came back empty.
When you need one, and what it actually enforces
Framing the globe on a site that is not ours takes a key. There are exactly two exceptions, and they exist so the thing can be tried before it is bought: our own pages, which is what makes the examples on this page and the preview in the builder work, and localhost or 127.0.0.1 at any port, which is your development loop. A page we cannot identify at all, because it sent no Referer, is refused too: we cannot check that it is ours, and "cannot say" is not allowed to resolve to "allow".
The refusal is a rendered message rather than a broken frame, and it costs nobody anything: it makes no upstream call, spends no quota, and names the console you mint a key in. Keys are free.
Be precise about what that requirement bought, because it is easy to oversell. It decides who can frame our globe in a browser, and it makes the traffic attributable to an account we can talk to and a key we can revoke. It is not a security control and it does not make the origin check into one: the rest of this section is unchanged, and still true.
An embed key is publishable. It sits in your HTML where every visitor can read it, and that is fine by design. What matters is being precise about which of its protections are real and which are only advisory, so you do not build a security assumption on the wrong one.
That is also why the console never hides it from you. Your pk_earth_live_ key is listed in full with a copy button, as often as you want it, because a value your own page hands to every visitor gains nothing by being masked in the one place its owner is entitled to read it. A secret key, sk_earth_live_, is the opposite class and follows the opposite rule: it is shown once when it is created and never again, it is stored only as a hash, and it travels in an Authorization header from your server. Nobody but you should ever hold that one, so nobody, us included, can read it back to you.
One honest exception. A publishable key minted before the console began keeping a readable copy exists only as a hash, so there is no plaintext to show and nobody can recover it. Those rows say exactly that rather than showing you a blank, and rotating gives you a key you can copy while the old one keeps working for 24 hours, which is time enough for the pages already carrying it to catch up.
| Control | How strong | Where it holds |
|---|---|---|
| Scope allowlist, rate limit, daily and monthly quota, per-visitor request budget, revocation, bounding box and result caps | Real. Our server enforces it. | Everywhere, including curl and any script. |
Access-Control-Allow-Origin and frame-ancestors | Real, but a browser enforces it, not us. | Inside a browser only. They stop a hostile page reading the response and stop a stolen key rendering our UI on an unregistered site. They are irrelevant to curl. |
The Origin, Referer, Sec-Fetch-Site and Sec-Fetch-Dest headers | Advisory. Never a boundary. | Honest from a real browser, one flag to forge from a script. They buy developer experience and defense in depth, nothing more. |
The part most docs leave out
A spoofed Origin or Referer buys nothing and stops nothing. Your registered origin is checked exactly once, on our server, at the moment the frame's token is minted. It is not re-checked on the data requests that token then makes, and it could not be: an embed token is spent from inside our own iframe, so the browser sends our origin on those calls, not your page's. That single check reads the framing origin the host page reported, which a script sets to whatever it likes, and curl -H "Origin: https://yoursite.com" sails straight through. So if someone copies your publishable key out of your page, the origin registration is not what stops them. Read it as the thing that keeps your key from rendering our UI on somebody else's site in a real browser, never as something that stops a script.
What actually bites is scope, the rate limit, the daily and monthly quota, the per-visitor request budget, and revocation. That is why an embed key can only reach the read endpoints listed in its scope set and can never reach a cost-bearing one: unlimited scripted use of a stolen key has to be merely annoying, never expensive and never a data breach. If a key does start showing traffic you do not recognize, revoke it in the console, which takes effect immediately with no grace window. That same list is what makes reading your own key back in full cost nothing. A publishable key was never defended by being hard to obtain, because your page gives it to every visitor who opens the source; it is defended by the controls in this paragraph, and not one of them is weakened by the owner copying it out of the console.
On that budget specifically: it is counted per minted token and per calling address, which is the closest thing to one visitor we can see without setting a cookie. It is not one pool shared by your whole audience, so a single busy reader cannot spend everybody else's. Its size is whatever your plan's per-minute ceiling could legitimately spend in the token's one-hour life, never below a fixed floor, so an honestly polling frame never meets it. A token renewal carries the spend forward rather than handing out a fresh budget every hour.
Register origins as scheme plus host, for example https://example.com. No paths, no wildcards. If your site answers on both the apex and www, register both: they are different origins to a browser.
One consequence of that is worth knowing before it costs you an afternoon. The keyless allowance for localhost is not a keyed one: once a key is in the tag, that key is checked against the page framing it, and your laptop is a page like any other. So while you develop, either leave data-key out, which is exactly what that allowance is for, or register http://localhost:3000 on the key alongside your production origin. A key that lists only production is refused on your machine, and the frame names the origin it saw so the cause is not a mystery.
Usage figures in the console are counted at our origin. Keyed responses are marked private, no-store, so keyed usage is exact. Keyless embed loads can be served by a cache and never reach the meter, so an unkeyed figure is a floor rather than an exact count. The console labels which is which.
Talking to the frame
The frame posts messages to your page and accepts a few back. Every message carries type, a version v, and a payload. With the script tag you can use SolyzerEmbed.on(type, handler) instead of wiring the listener yourself.
From the frame to you
| Type | What it means |
|---|---|
| solyzer:embed:ready | The frame has mounted and drawn. Payload carries keyless, the layers actually loaded, and any layer ids that were rejected. |
| solyzer:embed:resize | Reserved, and nothing sends it today. The frame fills its box at 100dvh, so it has no intrinsic content height to report. The loader would apply one under data-height="auto" if it ever arrived; until then use data-aspect. |
| solyzer:embed:camera | The visitor moved the map. Payload is lat, lon and zoom after the move settles. |
| solyzer:embed:select | The visitor picked a feature. Payload is kind, id, label, lat and lon. There is no other data in it: the frame does not hand out the layer payload. |
| solyzer:embed:error | Something the frame could not do. Payload is a machine code and a sentence written for a human. |
| solyzer:embed:unavailable | One or more layers could not be read this cycle, each with its reason. This is a named absence, not zero. Render it as "cannot say" if you surface it at all. |
| solyzer:embed:theme | The frame settled on a theme, sent on first paint and again whenever it changes. Payload is theme, dark or light, and source: config means your explicit data-theme decided, host means the theme your page reported decided, system means the visitor's browser decided. The source is there so you can tell whether what you sent was honoured or overruled. |
From you to the frame
An inbound message is a request, never authority. It can ask the camera to move; it can never grant a layer, a scope or a key. Every data call inside the frame is re-authorized on our server regardless of what you send.
| Type | Payload | What it does |
|---|---|---|
| solyzer:embed:flyTo | { lat, lon, zoom? } | Move the camera. |
| solyzer:embed:setLayers | { layers: string[] } | Ask for a different layer set. It is intersected with what your key already allows; it can never widen it. |
| solyzer:embed:setTheme | { theme: "dark" | "light" | "auto" } | Tell the frame what your page's theme is. The loader posts this for you; send it yourself if you have a custom toggle and are using the plain iframe. It loses to an explicit data-theme every time. auto means you no longer know, which hands resolution back to the frame instead of freezing it on what you said last. |
| solyzer:embed:refresh | none | Re-fetch the current layers now instead of waiting for the interval. |
Check both the origin and the source before you trust a message. Any page can post to yours, so a listener that only checks type will act on a forgery.
const FRAME_ORIGIN = "https://www.solyzer.ai";
const frame = document.querySelector("iframe");
window.addEventListener("message", (event) => {
if (event.origin !== FRAME_ORIGIN) return;
if (event.source !== frame.contentWindow) return;
const msg = event.data;
if (!msg || msg.type !== "solyzer:embed:select") return;
console.log(msg.payload.label, msg.payload.lat, msg.payload.lon);
});
// Ask the frame to move.
frame.contentWindow.postMessage(
{ type: "solyzer:embed:flyTo", v: 1, payload: { lat: 1.3521, lon: 103.8198, zoom: 11 } },
FRAME_ORIGIN
);It follows your page
data-theme defaults to auto, and auto means the frame works out where it is instead of assuming. It resolves in this order, and the first answer wins.
- 1An explicit data-theme of dark or light, which nothing overrides.
- 2The host page's theme, detected by the loader script and posted into the frame.
- 3prefers-color-scheme inside the frame, which is the visitor's browser setting.
- 4Dark.
What each embed form can see
The plain iframe has no script of ours on your page, so it cannot read your site at all. It follows the visitor's browser through prefers-color-scheme, which is the operating system setting, and it does that with no attribute and no configuration. If your site is dark for everyone regardless of their machine, that is the case where the two disagree, and pinning data-theme="dark" is the answer.
The script tag can do better, because it runs in your document. It reads your page's own theme and posts it into the frame, so the globe matches your SITE rather than the visitor's machine, and it keeps watching: flip your own light and dark toggle and the frame follows within a tick. There is no API call for that and deliberately is not one. You change your page, we follow.
It also puts the answer on the frame URL before the frame loads, as hosttheme, so the first paint is already right. Waiting for the handshake would mean a dark rectangle sitting on a light page for a moment, which is most of what people mean when they say an embed does not match their site. That parameter is the loader's to write and yours to ignore; to make a decision, write data-theme.
Whatever it lands on, the frame says so: solyzer:embed:theme carries the theme it settled on and which step decided, so a page that mirrors the map can, and a page that reported its own theme can tell whether that was honoured or overruled by an explicit data-theme.
Pinning it
Write data-theme="dark" or data-theme="light" and that is the end of it. Nothing detected on your page and nothing about the visitor overrides an explicit value, in either direction, ever. On the iframe form the same choice is the theme query parameter.
<!-- follows your site, then the visitor's browser. this is the default -->
<div data-solyzer-earth data-lat="-6.2088" data-lon="106.8456"></div>
<!-- pinned. nothing changes it -->
<div data-solyzer-earth data-lat="-6.2088" data-lon="106.8456" data-theme="dark"></div>Driving it yourself
With the script tag you do not need this: your toggle already changes your page, and the loader is watching your page. It is for the plain iframe, or for a toggle that lives somewhere our detection cannot see it.
frame.contentWindow.postMessage(
{ type: "solyzer:embed:setTheme", v: 1, payload: { theme: "light" } },
"https://www.solyzer.ai"
);
// handing resolution back, for a toggle that has a "system" position
frame.contentWindow.postMessage(
{ type: "solyzer:embed:setTheme", v: 1, payload: { theme: "auto" } },
"https://www.solyzer.ai"
);Where the detection is only a guess
Reading someone else's theme is inference, so it is worth knowing what it looks at and where it stops. In order: the computed color-scheme on your html or body, which is the one signal that is a declaration rather than a deduction; then a dark or light class or a data-theme attribute on those elements, which is what Tailwind, next-themes and most design systems set; then the computed background colour, converted to relative luminance.
The case it cannot solve is a page whose look comes from something other than a background colour: a background image, a gradient, or a full-bleed element painted over a transparent body. A transparent background is reported as unknown rather than read as white, because that is exactly the situation where what the visitor sees is something the loader cannot see, and a confident wrong answer puts a light globe on a dark page. When nothing resolves, nothing is sent and the frame falls back to the visitor's browser.
Two ways to make it certain rather than inferred, both one line. Set color-scheme on your own page, which is worth doing anyway because it also fixes your scrollbars and form controls. Or pin data-theme and stop the guessing entirely.
:root { color-scheme: dark; } /* or light, or "light dark" if you follow the visitor */Size, shape, and what you cannot restyle
The frame is cross-origin, so your CSS does not reach inside it. You style the box, we style the map. Matching your page is the theme section above, and it is automatic. There is no ground option: the globe draws the same CARTO fabric at every zoom, so an attribute for it would change nothing.
Give it a real height. A frame in a zero-height box produces a canvas that never paints, which looks like a broken embed and is really just a collapsed parent. Either set data-height to a pixel value, or set data-aspect to a ratio such as 16/9, which is the answer for responsive sizing.
data-height="auto" is accepted but has nothing to measure. The framed document fills its box at 100dvh, so it has no intrinsic content height to report, and nothing in it posts solyzer:embed:resize today. That message type is reserved: the loader applies a height if one ever arrives, so auto currently just keeps the 520 px fallback. Use data-aspect instead.
Below 240 by 180 CSS pixels the frame refuses to draw the map and shows a line saying why. At that size the source credit does not fit, and shipping an uncredited map is not something we will do quietly.
.earth-embed {
border: 1px solid #26262a;
border-radius: 12px;
overflow: hidden; /* clips the frame's own corners */
}On the sandbox attribute in the iframe snippet: allow-scripts allow-same-origin together is close to no sandbox at all for our own document, and we would rather say that than let a security reviewer discover it. What it does still remove is form submission, top-level navigation, pointer lock and downloads. Drop the attribute entirely if your reviewer prefers an honest absence to a misleading presence.
The credit line is not optional
The credit line, © OpenStreetMap · CARTO · Overture, is on screen in every frame that draws a map. It is static text with no close control, and no attribute removes it. That line is the attribution.
Next to it is a licences link that opens the full per-source licence list. That list starts folded, because opened it covers half of a 520 px embed, and an attribution that hides the map it is attributing helps nobody: the site drops the embed and the sources end up credited nowhere. The link is always offered, on every frame, whatever you pass in data-ui, so a reader who wants the detail never depends on the embedding site having opted them in.
That is not house style, it is the license. Several of the layers are ODbL 1.0 share alike, including OpenStreetMap by way of OpenFreeMap, the OpenStreetMap-derived Overture layers, and the aircraft feeds from adsb.fi and adsb.lol. ODbL binds the data, not only the tiles, and your page is a produced work in public use.
Satellite imagery is not included. Esri World Imagery is excluded pending a licensing answer and Esri Clarity is excluded outright with no override, so overlay:satellite in an embed falls back to the CARTO ground the globe always draws. An endpoint that answers without a key is not a license to redistribute its imagery on a third party's monetized page. If you need imagery, supply your own ArcGIS key, which also puts the license obligation with the party that holds the license.
One honest limit: a cross-origin frame cannot see what your page draws on top of it, and visibility tracking is best effort by specification. We cannot detect an overlay that hides the credit, so that is a contractual term plus a manual audit of top referring domains rather than something code solves. Do not read the absence of an error as permission.
Two lines, and only one of them always
If your page sends a Content Security Policy, it has to permit the frame. This line is required for both the script tag and the plain iframe.
frame-src https://www.solyzer.ai;This second line is required only for the script tag form, because that is the only form that loads a file of ours into your document.
script-src https://www.solyzer.ai;Add them to your existing directives rather than replacing them, and remember that two Content Security Policy headers on one response intersect: the narrower one wins on every directive.
Coming the other way, the frame sends its own frame-ancestors naming the origins you registered. That is what makes an unregistered domain fail visibly instead of loading a free map. Two cases send no frame-ancestors clause at all: a keyless frame, which is now only served to our own pages and to localhost and has no registered domain list to name, and every refusal, including the one a keyless embed gets on your site. A refusal is deliberately left framable, because locking down a frame whose whole job is to show you the sentence that fixes it would replace the sentence with a blank box.
When it does not look right
The frame says this embed needs a key
There is no data-key on the container, or no k on the frame URL, and the page framing it is not one of ours and not localhost. Mint a free publishable key in the builder, register this page's origin on it, and add it as data-key. Nothing was fetched and no allowance was spent: the load was refused before any upstream call.
It rendered on localhost and refuses on my live site
That is the keyless allowance ending, and it is working as intended. Localhost and 127.0.0.1 render without a key so you can build; every other origin needs one. Add data-key with a key that lists your live origin.
The frame says the key is not one of ours
Usually the placeholder from the docs snippet shipped as-is. pk_earth_live_YOUR_KEY is a hole to fill, not a key. Paste a real one from the builder, which lists your publishable keys in full with a copy button.
The frame is blank and the console says the frame was refused
Your page's origin is not on the key's list, so the frame's frame-ancestors did not include it. Add the exact origin, scheme included, in the builder. The apex and the www host are different origins.
The frame renders but says the origin is not registered
The key resolved and the load was refused before any upstream call, so it did not consume your quota. The line names the origin it saw; register that exact string.
The globe is dark and my page is light
Nothing on your page resolved to a theme, so the frame fell back to the visitor's browser, which is set to dark. That happens when your background comes from an image, a gradient or an element painted over a transparent body, because a transparent background is read as unknown rather than as white. Two one-line fixes: set color-scheme on your own page, or pin data-theme="light" on the container. With the plain iframe there is no script of ours on your page at all, so the visitor's browser is the only signal there is.
My theme toggle changes the page but not the embed
The loader watches the class, the data-theme attribute and the inline style on your html and body elements. A toggle that changes something else, a stylesheet swap or a class on a wrapper div further down, is invisible to it. Either move the marker onto html, which is where Tailwind and next-themes already put it, or post solyzer:embed:setTheme to the frame from your toggle. The plain iframe form has no loader, so it never follows a site toggle and always needs that message.
Nothing renders and there is no message at all
Almost always a collapsed parent. Give the container a height or an aspect ratio. A frame in a zero-height box produces a canvas that never paints.
The loader wrote a failure line after about twelve seconds
The frame never sent solyzer:embed:ready, which is what a refused load looks like from outside: the message is inside the frame, so read that first. Opening the frame URL directly in a tab shows a reason too, with one caveat worth knowing: a direct visit has no framing page at all, so a URL with no key always reports that it needs one, even for us. The loader deliberately never draws a placeholder map, because a fake map is worse than a visible failure.
A layer I asked for is missing
Look at the frame. A layer id we do not recognize is named back to you as rejected, and a layer that could not be read this cycle is named as unavailable with its reason. Neither is drawn as an empty map. Unavailable means we could not say, which is not the same as nothing is there.
It worked yesterday and now everything is refused
Check whether the key was rotated or revoked. A rotation gives the old publishable key a grace window because it lives in caches you do not control; a revoke is immediate with no grace at all.
I lost the key
A publishable key is not something you can lose. Open the builder and it is there in full, with a copy button, as many times as you need it: it is public by design and already readable in your own page's source, so there is nothing gained by keeping it from you. The exception is a key minted before the console began keeping a readable copy, which exists only as a hash and genuinely cannot be recovered; that row says so instead of showing a blank, and rotating gives you one you can copy while the old key keeps working for 24 hours. A secret key is the other class and the other rule: sk_earth_ keys are shown once at creation and never again, so a lost one is rotated, not recovered.