Embedding overview
Signal provides one complete embedded application, mounted once inside your product. Its internal navigation includes the permitted overview, locations, prompts, citations, competitors, and settings screens; those screens are not mounted as separate components. A short-lived session determines the project, end user, and access available in the application. Standard projects open at project-level visibility pages. Locations-only projects retain the location portfolio and map experience.
https://api.signal.ceyo.ai/v1/embed/sessionsHow embedding works
Ceyo.mount() once.The Signal API key stays on your backend. The browser receives only a short-lived, user-scoped session token.
Embed sessions expose only viewer or editor access. Hosted admin privileges—including partner-managed user administration—are available only through redirect login and are never passed into the iframe.
Provisioning and scope
Every session represents one external user and a project, one location, or that identity's directly granted locations in a project. Create projects and locations, choose their packages, and start onboarding through the public API before loading the embeddable. The iframe does not create or delete either resource. You can provision the identity and grant explicitly, then mint sessions as needed:
# 1. Upsert the partner-managed identity.
POST /v1/embedded-identities/user-42
{
"email": "user-42@example.com",
"name": "User 42"
}
# 2. Grant access for the scope this session will use.
POST /v1/embedded-identities/user-42/projects/partner-project/access
{
"role": "editor"
}
# 3. Mint the short-lived browser session.
POST /v1/embed/sessions
Idempotency-Key: <unique-value>
{
"external_user_id": "user-42",
"project_id": "partner-project",
"ttl_seconds": 3600
}For first login, the session endpoint can perform those first two steps atomically through provision, as shown in the quickstart. Later calls without provision reuse the existing identity and grant.
location_id. The session opens project-level prompts, citations, and competitors for a standard project, or its permitted locations for a locations-only project.location_id when minting. The session is restricted to that location.location_scope: "granted" and omit location_id. The granted_locations browser scope exposes only Overview and Locations. It resolves active direct grants live, preserves each location's role, and ignores any project grant. Project-wide routes are unavailable.Pre-granted identity
POST /v1/embed/sessions
{
"external_user_id": "regional-manager-42",
"project_id": "partner-project",
"location_scope": "granted",
"ttl_seconds": 3600
}Atomic first-login location grants
POST /v1/embed/sessions
{
"external_user_id": "regional-manager-42",
"project_id": "partner-project",
"location_scope": "granted",
"provision": {
"identity": {
"email": "manager@example.com",
"name": "Regional Manager"
},
"location_grants": [
{ "location_id": "store-amsterdam", "role": "editor" },
{ "location_id": "store-utrecht", "role": "viewer" }
]
}
}Batch provisioning accepts one to 100 direct location grants. Revoking a grant removes that location from the live portfolio; when one location remains, configured single-location auto-open still applies.
Embedding quickstart
1. Mint a session on your backend
Send the signed-in user's stable external ID and the target project to the embed session endpoint. The example uses inline provisioning so the same atomic request can create the identity, ensure its grant, and issue the session.
// Runs on your server. Never send the API key to the browser.
app.post("/api/signal-session", async (req, res) => {
const response = await fetch("https://api.signal.ceyo.ai/v1/embed/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SIGNAL_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
project_id: process.env.SIGNAL_PROJECT_ID,
external_user_id: req.user.id,
ttl_seconds: 3600,
provision: {
role: req.user.canEditSignal ? "editor" : "viewer",
identity: {
email: req.user.email,
name: req.user.name,
},
},
}),
});
if (!response.ok) {
return res.status(response.status).json(await response.json());
}
const { session } = await response.json();
res.json({ token: session.token, expires_at: session.expires_at });
});
app.post("/api/signal-session/refresh", async (req, res) => {
const response = await fetch("https://api.signal.ceyo.ai/v1/embed/sessions/refresh", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SIGNAL_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": req.get("Idempotency-Key") || crypto.randomUUID(),
},
body: JSON.stringify({
session_token: req.body.session_token,
ttl_seconds: 3600,
}),
});
const body = await response.json();
if (!response.ok) return res.status(response.status).json(body);
res.json({ token: body.session.token, expires_at: body.session.expires_at });
});2. Load and mount in the browser
<div id="signal-visibility"></div>
<script src="https://cdn.signal.ceyo.ai/embed/v1.js"></script>
<script>
async function createSession() {
const response = await fetch("/api/signal-session", { method: "POST" });
if (!response.ok) throw new Error("Unable to create embed session");
const session = await response.json();
return { token: session.token, expiresAt: session.expires_at };
}
(async () => {
let currentSession = await createSession(); // Keep tokens in memory.
let embed;
let refreshInFlight;
async function refreshSession(currentToken, updateHandle) {
if (currentSession.token !== currentToken) return currentSession.token;
if (refreshInFlight) return refreshInFlight;
refreshInFlight = (async () => {
const response = await fetch("/api/signal-session/refresh", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ session_token: currentToken }),
});
const session = response.ok
? await response.json()
: await createSession();
if (currentSession.token !== currentToken) return currentSession.token;
currentSession = {
token: session.token,
expiresAt: session.expiresAt || session.expires_at,
};
if (updateHandle) embed.updateToken(currentSession.token);
return currentSession.token;
})().finally(() => {
refreshInFlight = null;
});
return refreshInFlight;
}
embed = Ceyo.mount("#signal-visibility", {
sessionToken: currentSession.token,
height: "780px",
onTokenExpired() {
return refreshSession(currentSession.token, false);
},
});
window.signalEmbed = embed;
})();
</script>