Listings embeddable
Embeddable Listing Visibility Widget
Add a business listing scan to a customer-facing page. The host page passes a publishable widget key, theme, and settings; the widget handles listing search, scan creation, lead capture, and report display.
Give these docs to your AI agent
Copy or download a clean markdown version of the Listing Visibility embed docs.
publicKey and set settings.product = "listing_visibility" when mounting the widget.How it works
| Part | Where it runs | What it does |
|---|---|---|
| Loader | your page | Creates and manages the iframe, then passes the widget key, theme, and settings to it. |
| Widget key | browser | Publishable key that identifies the workspace and allowed origins for this widget. |
| Widget app | iframe | Renders the Listing Visibility scan flow and report. |
| Host page | your site | Controls placement, height, theme, copy, and callbacks. |
Quick start
Create a widget key in Ceyo and add the domains where the widget can run.
Add a container where the widget should appear.
Include the loader once on the page.
Call
Ceyo.mount()withpublicKeyandsettings.product = "listing_visibility".
<div id="ceyo-listing-visibility"></div>
<script src="https://cdn.ceyo.ai/embed/v1.js"></script>
<script>
const embed = Ceyo.mount("#ceyo-listing-visibility", {
publicKey: "ceyo_lpk_...",
height: "760px",
title: "AI Visibility Scan",
settings: {
product: "listing_visibility",
title: "AI Visibility Scan",
subtitle: "See how your business listing appears across search and answer engines."
}
});
</script>Widget key
Create a Listing Visibility widget key in Ceyo. The key is publishable and is designed to be used in browser code.
| Value | Meaning |
|---|---|
publicKey | The publishable key passed to Ceyo.mount(). |
allowed_origins | Origins where the widget is allowed to run. |
enabled | Disable the key to stop new sessions for that widget. |
Allowed origins
Allowed origins must be exact origins: scheme, host, and optional port. Do not include paths, query strings, or fragments.
| Input | Valid? | Notes |
|---|---|---|
https://example.com | Yes | Production domain. |
https://app.example.com | Yes | Subdomains are separate origins. |
http://localhost:3000 | Yes | Useful while building your integration locally. |
https://example.com/page | No | Remove the path. |
Install the loader
Include the loader script once per page:
<script src="https://cdn.ceyo.ai/embed/v1.js"></script>The loader registers a single global, Ceyo, and only runs when you call Ceyo.mount().
Mount the widget
Ceyo.mount(target, options) renders the widget into a container element. The iframe fills the container width; you control the height.
const embed = Ceyo.mount("#ceyo-listing-visibility", {
publicKey: "ceyo_lpk_...",
height: "760px",
minWidth: "320px",
title: "AI Visibility Scan",
settings: {
product: "listing_visibility",
title: "AI Visibility Scan",
subtitle: "See how your business listing appears across search and answer engines."
},
theme: {
colors: {
primary: "#0F766E",
primaryText: "#FFFFFF",
background: "transparent",
surface: "#FFFFFF",
text: "#16181D",
muted: "#6B7280",
border: "#E5E7EB",
warning: "#B45309",
warningSurface: "#FFFBEB"
},
shape: {
radius: "10px",
cardRadius: "12px",
controlRadius: "10px"
},
density: "compact"
},
onReady: () => console.log("listing widget ready"),
onError: (message) => console.error("listing widget error:", message)
});Mount options
| Name | Type | Required | Description |
|---|---|---|---|
| publicKey | string | Yes | Publishable widget key from Ceyo. |
| settings.product | string | Yes | listing_visibility. This tells the shared loader to open the Listing Visibility scan widget. |
| height | string | No | Iframe height. Defaults to 720px. Width is always 100%. |
| minWidth | string | No | Minimum iframe width. Defaults to 360px. Use 320px for narrow mobile containers. |
| title | string | No | Accessible title for the iframe element. Defaults to Visibility. |
| theme | object | No | Visual overrides for colors, typography, shape, spacing, and density. |
| settings | object | No | Widget copy and product settings. |
| onReady | function | No | Called once after the widget has booted. |
| onError | function | No | Called with a message when the widget reports an error. |
Widget handle
Ceyo.mount() returns a handle:
| Member | Type | Description |
|---|---|---|
| iframe | HTMLIFrameElement | The managed iframe element. |
| unmount() | function | Remove the widget and all event listeners. |
Full example
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Listing Visibility Scan</title>
</head>
<body>
<main>
<h1>Check your AI visibility</h1>
<p>Find your business listing and run a visibility scan.</p>
<div id="ceyo-listing-visibility"></div>
</main>
<script src="https://cdn.ceyo.ai/embed/v1.js"></script>
<script>
const embed = Ceyo.mount("#ceyo-listing-visibility", {
publicKey: "ceyo_lpk_...",
height: "760px",
minWidth: "320px",
title: "AI Visibility Scan",
settings: {
product: "listing_visibility",
title: "AI Visibility Scan",
subtitle: "See how your business listing appears across search and answer engines."
},
theme: {
colors: {
primary: "#0F766E",
primaryText: "#FFFFFF"
}
},
onReady: () => {
console.log("Listing widget ready");
},
onError: (message) => {
console.error("Listing widget error:", message);
}
});
// Later, if needed:
// embed.unmount();
</script>
</body>
</html>Settings
Listing Visibility uses a small settings object. product is required; the rest controls visible copy.
| Name | Type | Required | Description |
|---|---|---|---|
| product | string | Yes | listing_visibility. |
| title | string | No | Widget heading. Defaults to AI Visibility. |
| subtitle | string | No | Short intro text below the heading. |
Theme
Theme overrides are plain tokens. Anything you do not set keeps its default.
theme: {
colors: {
primary: "#0F766E",
primaryText: "#FFFFFF",
background: "transparent",
surface: "#FFFFFF",
text: "#16181D",
muted: "#6B7280",
border: "#E5E7EB",
success: "#047857",
successSurface: "#ECFDF5",
warning: "#B45309",
warningSurface: "#FFFBEB",
danger: "#B91C1C",
dangerSurface: "#FEF2F2"
},
typography: {
fontFamily: "'Inter', sans-serif",
baseSize: "14px",
smallSize: "12.5px",
titleSize: "22px",
titleWeight: "700",
bodyWeight: "400"
},
shape: {
radius: "10px",
cardRadius: "10px",
controlRadius: "10px",
pillRadius: "7px"
},
spacing: {
pagePadding: "20px",
panelPadding: "16px",
cardPadding: "16px 18px",
gap: "12px",
controlHeight: "34px"
},
density: "compact"
}colors
| Token | Default | Description |
|---|---|---|
| primary | #16181d | Buttons, active states, and accents. |
| primaryText | #ffffff | Text on primary surfaces. |
| background | transparent | Widget page background. |
| surface | #ffffff | Cards and panels. |
| text | #16181d | Primary text. |
| muted | #6b7280 | Secondary text. |
| border | #e5e7eb | Borders and dividers. |
| success / successSurface | #047857 / #ecfdf5 | Positive indicators. |
| warning / warningSurface | #b45309 / #fffbeb | Warning indicators, star/rating accents, and mid-range score states. |
| danger / dangerSurface | #b91c1c / #fef2f2 | Negative indicators. |
typography
| Token | Default | Description |
|---|---|---|
| fontFamily | system stack | Set this to match your product font. |
| baseSize | 14px | Body text size. |
| smallSize | 12.5px | Secondary text size. |
| titleSize | 22px | Widget title size. |
| titleWeight / bodyWeight | 700 / 400 | Font weights. |
shape & density
| Token | Default | Description |
|---|---|---|
| shape.radius | 10px | Base border radius. |
| shape.cardRadius | 10px | Card radius. |
| shape.controlRadius | 10px | Inputs and buttons. |
| shape.pillRadius | 7px | Badges and pills. |
| density | compact | compact or comfortable. Adjusts paddings and control heights as a set. |
spacing
| Token | Default | Description |
|---|---|---|
| pagePadding | 20px | Outer page padding. |
| panelPadding | 16px | Panel padding. |
| cardPadding | 16px 18px | Card padding. |
| gap | 12px | Default layout gap. |
| controlHeight | 34px | Input and button height. |
Events
| Callback | When it runs |
|---|---|
onReady() | The iframe has booted and received its initial configuration. |
onError(message) | The widget reports a setup or runtime error. |
Limits
| Limit | What happens |
|---|---|
| Daily per-IP scan limit | The widget can stop additional scans from the same IP for the day and show a retry message. |
| Monthly workspace scan limit | If the workspace reaches its monthly scan allowance, the widget shows a scan limit reached state. |
| Allowed origins | If the page origin is not allowed for the widget key, setup fails and onError is called. |
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| Widget does not load | Check that the loader script is present, the mount target exists, and publicKey is set. |
| Origin is not allowed | Add the exact page origin to the widget key allowed origins. |
| Scan limit reached | The workspace reached its monthly scan allowance. |
| Too many scans | The visitor reached the widget scan limit for their IP. Try again later. |
| Widget is too wide on mobile | Pass minWidth: "320px" or give the host container enough width. |