Embed sessions
Issue short-lived browser credentials for an embedded user, then rotate them without changing the user’s effective access.
provision to create or update the identity and ensure the scope grant or a batch of location grants atomically before issuing the session.SIGNAL_API_KEY as the bearer credential. The key requires the embed_sessions:create capability. Inline provisioning additionally requires a workspace-scoped key with identities:manage. Send only the returned embed session token to the browser. Never expose the API key in JavaScript, HTML, logs, URLs, or mobile application code.POSTCreate embed session
/embed/sessionsCreates a short-lived embed session using existing access or optional atomic first-login provisioning.
Create embed session
/embed/sessionsRequest body
external_user_idstringStable, case-sensitive identifier for the signed-in user in your system. It must already exist unless provision is supplied.
project_idproject UUID | project external IDCeyo project UUID or configured partner external ID. The project must belong to the workspace selected by the API key.
location_idlocation UUID | location external IDRestricts the session to one location in the project. The identity must have access unless provision ensures the grant. Omit for project scope.
location_scopegrantedExposes only the identity’s active direct location grants in this project. Mutually exclusive with location_id.
ttl_secondsintegerSession lifetime in seconds. Minimum: 60. Maximum: 86400. Prefer the shortest lifetime suitable for your integration.
expires_inintegerCompatibility alias for ttl_seconds. Do not send both unless their values are identical.
provisionProvisionInputOptionally creates or updates the identity and ensures one project/location grant, or a granted-scope batch of location grants, before issuing the session.
ProvisionInput
roleviewer | editorRequired for project or single-location provisioning. Do not combine with location_grants.
identityProvisionIdentityInputOptional identity fields to set. Omitted fields remain unchanged on an existing identity.
location_grantsProvisionLocationGrantInput[]One to 100 direct location grants. Allowed only with location_scope granted and mutually exclusive with role.
ProvisionIdentityInput
emailstring | nullOptional normalized contact email.
namestring | nullOptional display name.
avatar_urlHTTPS URL | nullOptional absolute HTTPS avatar URL.
metadataobjectOptional partner metadata object, up to 16 KB.
ProvisionLocationGrantInput
location_idlocation UUID | location external IDRequired location in the selected project.
roleviewer | editorRequired role for this direct location grant. Roles may differ between locations.
location_id, the identity must have access to that location. With location_scope: "granted", only active direct location grants in the project are visible; a project grant does not expand the result. Grants are checked live, and each location keeps its own role. Sessions support only viewer and editor; hosted admin grants are capped to editor.EmbedSessionResponse
session.tokenstringOpaque, short-lived credential for the embedded browser component. Treat it as a secret.
session.token_typeBearerAuthentication scheme used by the embed session token.
session.expires_atdatetimeToken expiration time in ISO 8601 UTC format.
session.expires_inintegerSession lifetime in seconds.
session.project_iduuidCompatibility alias for session.scope.project_id.
session.location_iduuid | nullCompatibility alias for session.scope.location_id.
session.location_scopefull | location | grantedSession scope mode. granted resolves the identity’s active direct location grants live.
session.external_user_idstringCompatibility alias for session.identity.external_user_id.
session.identity.external_user_idstringPartner identifier of the embedded user represented by the session.
session.scope.typegrantedPresent for granted-location sessions.
session.scope.project_iduuidCanonical Ceyo UUID of the project available to the session.
session.scope.location_iduuid | nullCanonical Ceyo UUID of the restricted location, or null for project scope.
session.scope.roleviewer | editor | nullEffective role for project or single-location scope. Granted sessions use each location’s own role.
type EmbedSessionResponse = {
session: {
token: string;
token_type: "Bearer";
expires_at: string;
expires_in: number;
project_id: string;
location_id: string | null;
location_scope: "full" | "location" | "granted";
external_user_id: string;
identity: {
external_user_id: string;
};
scope: {
type?: "granted";
project_id: string;
location_id: string | null;
role: "viewer" | "editor" | null;
};
};
};Request and response
curl --request POST \
--url 'https://api.signal.ceyo.ai/v1/embed/sessions' \
--header "Authorization: Bearer ${SIGNAL_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: embed-create-user-42-20260731T123300Z' \
--data '{
"external_user_id": "user-42",
"project_id": "partner-project-acme",
"ttl_seconds": 3600
}'HTTP/1.1 201 Created
{
"session": {
"token": "ceyo_embed_eyJhbGciOi...",
"token_type": "Bearer",
"expires_at": "2026-07-31T13:33:00Z",
"expires_in": 3600,
"project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
"location_id": null,
"location_scope": "full",
"external_user_id": "user-42",
"identity": {
"external_user_id": "user-42"
},
"scope": {
"project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
"location_id": null,
"role": "viewer"
}
}
}Location-scoped request
curl --request POST \
--url 'https://api.signal.ceyo.ai/v1/embed/sessions' \
--header "Authorization: Bearer ${SIGNAL_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: embed-create-franchisee-804-20260731T125000Z' \
--data '{
"external_user_id": "franchisee-804",
"project_id": "partner-project-acme",
"location_id": "partner-location-amsterdam",
"ttl_seconds": 1800
}'Pre-granted location portfolio
curl --request POST \
--url 'https://api.signal.ceyo.ai/v1/embed/sessions' \
--header "Authorization: Bearer ${SIGNAL_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: embed-create-region-42-20260805T120000Z' \
--data '{
"external_user_id": "regional-manager-42",
"project_id": "partner-project-acme",
"location_scope": "granted",
"ttl_seconds": 3600
}'This session's scope type is granted. It exposes Overview and Locations only; project-wide prompts, citations, competitors, settings, and users are unavailable. Single-location auto-open behavior still applies.
First-login provisioning
curl --request POST \
--url 'https://api.signal.ceyo.ai/v1/embed/sessions' \
--header "Authorization: Bearer ${SIGNAL_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: embed-provision-new-user-42' \
--data '{
"external_user_id": "new-user-42",
"project_id": "partner-project-acme",
"ttl_seconds": 3600,
"provision": {
"role": "viewer",
"identity": {
"email": "new-user-42@example.com",
"name": "New User"
}
}
}'Atomic location batch provisioning
curl --request POST \
--url 'https://api.signal.ceyo.ai/v1/embed/sessions' \
--header "Authorization: Bearer ${SIGNAL_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: embed-provision-region-42' \
--data '{
"external_user_id": "regional-manager-42",
"project_id": "partner-project-acme",
"location_scope": "granted",
"provision": {
"identity": {
"email": "manager-42@example.com",
"name": "Regional Manager"
},
"location_grants": [
{
"location_id": "store-amsterdam",
"role": "editor"
},
{
"location_id": "store-utrecht",
"role": "viewer"
}
]
}
}'Supply one to 100 grants. The batch atomically ensures the listed direct grants; it does not add unlisted locations or make a project grant part of this scope.
Idempotency-Key header is required for each intended session. Repeating the same request with the same key returns the original response and does not issue another token. Reusing that key with different body fields returns 409. Use a new key when you intentionally need a separate session.ErrorsResponse format and status codes
Error response envelope
errorErrorStructured error payload.
Error
codestringStable snake_case code suitable for programmatic handling.
messagestringHuman-readable explanation of the failure.
detailsobject | array | nullStructured validation or request context when available.
request_idstringIdentifier to provide when requesting support.
{
"error": {
"code": "invalid_request",
"message": "The request could not be processed.",
"details": {
"field": "ttl_seconds"
},
"request_id": "req_01JEXAMPLE7J8Q2Y4K6M9"
}
}Status codes
400invalid_requestThe JSON body is malformed, required fields are absent, or a request object or TTL is invalid.
401invalid_api_keyThe platform API key is absent, invalid, expired, or revoked.
403forbidden | access_deniedThe API key lacks the required capability, or the identity is disabled or has no active access grant.
404identity_not_found | project_not_found | location_not_foundThe requested identity, project, or location was not found.
409idempotency_conflictThe idempotency key was reused with a different request.
422validation_failedThe inline identity, grant role, or access grant failed validation.
429rate_limit_exceededToo many requests were made; follow the Retry-After header.
POSTRefresh embed session
/embed/sessions/refreshAtomically exchanges a current embed session token for a replacement with the same identity and scope.
Refresh embed session
/embed/sessions/refreshRequest body
session_tokenstringCurrent embed session token. It may be active or expired by no more than five minutes. The token is exchanged and cannot be used again after a successful refresh.
ttl_secondsintegerLifetime of the replacement token in seconds. Minimum: 60. Maximum: 86400.
expires_inintegerCompatibility alias for ttl_seconds. Do not send both unless their values are identical.
Idempotency-Key header is required. A successful exchange invalidates the submitted session token. Subsequent exchanges of that token fail unless they repeat the completed request with the same Idempotency-Key, in which case the original replacement response is returned. An active token may be refreshed at any time and an expired token has a five-minute refresh window.EmbedSessionResponse
session.tokenstringOpaque, short-lived credential for the embedded browser component. Treat it as a secret.
session.token_typeBearerAuthentication scheme used by the embed session token.
session.expires_atdatetimeToken expiration time in ISO 8601 UTC format.
session.expires_inintegerSession lifetime in seconds.
session.project_iduuidCompatibility alias for session.scope.project_id.
session.location_iduuid | nullCompatibility alias for session.scope.location_id.
session.location_scopefull | location | grantedSession scope mode. granted resolves the identity’s active direct location grants live.
session.external_user_idstringCompatibility alias for session.identity.external_user_id.
session.identity.external_user_idstringPartner identifier of the embedded user represented by the session.
session.scope.typegrantedPresent for granted-location sessions.
session.scope.project_iduuidCanonical Ceyo UUID of the project available to the session.
session.scope.location_iduuid | nullCanonical Ceyo UUID of the restricted location, or null for project scope.
session.scope.roleviewer | editor | nullEffective role for project or single-location scope. Granted sessions use each location’s own role.
type EmbedSessionResponse = {
session: {
token: string;
token_type: "Bearer";
expires_at: string;
expires_in: number;
project_id: string;
location_id: string | null;
location_scope: "full" | "location" | "granted";
external_user_id: string;
identity: {
external_user_id: string;
};
scope: {
type?: "granted";
project_id: string;
location_id: string | null;
role: "viewer" | "editor" | null;
};
};
};Request and response
curl --request POST \
--url 'https://api.signal.ceyo.ai/v1/embed/sessions/refresh' \
--header "Authorization: Bearer ${SIGNAL_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: embed-refresh-session-7f42a-1' \
--data '{
"session_token": "ceyo_embed_eyJhbGciOi...current",
"ttl_seconds": 3600
}'HTTP/1.1 200 OK
{
"session": {
"token": "ceyo_embed_eyJhbGciOi...replacement",
"token_type": "Bearer",
"expires_at": "2026-07-31T14:20:00Z",
"expires_in": 3600,
"project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
"location_id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
"location_scope": "location",
"external_user_id": "franchisee-804",
"identity": {
"external_user_id": "franchisee-804"
},
"scope": {
"project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
"location_id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
"role": "editor"
}
}
}Backend refresh endpoint
// Authenticate your application user before this handler.
// Your platform API key remains on the server.
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,
});
});Browser token lifecycle
Keep the active token and its expiresAt value in memory. Before expiration, send the current token to your authenticated backend, atomically replace the in-memory record, and call embed.updateToken(). The expiry callback should exchange your in-memory current token and return the replacement token. Returning null leaves the application locked.
let currentSession = {
token: initialToken,
expiresAt: initialExpiresAt,
};
async function refreshSession(
currentToken: string,
updateHandle: boolean,
): Promise<string | null> {
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 }),
});
if (!response.ok) return null;
const session = await response.json();
currentSession = {
token: session.token,
expiresAt: session.expires_at,
};
if (updateHandle) embed.updateToken(currentSession.token);
return currentSession.token;
}
const embed = Ceyo.mount("#signal-visibility", {
sessionToken: currentSession.token,
onTokenExpired() {
return refreshSession(currentSession.token, false);
},
});
// Schedule this before currentSession.expiresAt.
await refreshSession(currentSession.token, true);ErrorsResponse format and status codes
Error response envelope
errorErrorStructured error payload.
Error
codestringStable snake_case code suitable for programmatic handling.
messagestringHuman-readable explanation of the failure.
detailsobject | array | nullStructured validation or request context when available.
request_idstringIdentifier to provide when requesting support.
{
"error": {
"code": "invalid_request",
"message": "The request could not be processed.",
"details": {
"field": "ttl_seconds"
},
"request_id": "req_01JEXAMPLE7J8Q2Y4K6M9"
}
}Status codes
400invalid_requestThe JSON body is malformed, required fields are absent, or a request object or TTL is invalid.
401invalid_api_key | invalid_session_tokenThe API key or session token is invalid, revoked, expired, or outside the refresh window.
403session_workspace_mismatch | access_deniedThe session belongs to another workspace or its identity no longer has access.
409idempotency_conflictThe idempotency key was reused with a different request.
429rate_limit_exceededToo many requests were made; follow the Retry-After header.
Security checklist
external_user_id from an unauthenticated browser.