Signal API
Embed sessions as MarkdownUse a clean Markdown version of this page with an agent or local tooling.

Embed sessions

Issue short-lived browser credentials for an embedded user, then rotate them without changing the user’s effective access.

Embed session paths never contain a workspace identifier. The platform API key selects the workspace, while each request selects one embedded identity and one project, location, or direct-grant portfolio scope.
Provisioning optionsBy default, the embedded identity and its project or location grants must already exist. For a first-login flow, send provision to create or update the identity and ensure the scope grant or a batch of location grants atomically before issuing the session.
Server-to-server authenticationCall both endpoints from your backend with 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.
POST
/embed/sessions

Creates a short-lived embed session using existing access or optional atomic first-login provisioning.

Parameters

Request body

external_user_idstring

Stable, case-sensitive identifier for the signed-in user in your system. It must already exist unless provision is supplied.

Required
project_idproject UUID | project external ID

Ceyo project UUID or configured partner external ID. The project must belong to the workspace selected by the API key.

Required
location_idlocation UUID | location external ID

Restricts the session to one location in the project. The identity must have access unless provision ensures the grant. Omit for project scope.

Optional
location_scopegranted

Exposes only the identity’s active direct location grants in this project. Mutually exclusive with location_id.

Optional
ttl_secondsinteger

Session lifetime in seconds. Minimum: 60. Maximum: 86400. Prefer the shortest lifetime suitable for your integration.

OptionalDefault: 3600
expires_ininteger

Compatibility alias for ttl_seconds. Do not send both unless their values are identical.

Optional
provisionProvisionInput

Optionally creates or updates the identity and ensures one project/location grant, or a granted-scope batch of location grants, before issuing the session.

Optional
Request

ProvisionInput

roleviewer | editor

Required for project or single-location provisioning. Do not combine with location_grants.

identityProvisionIdentityInput

Optional 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.

Request

ProvisionIdentityInput

emailstring | null

Optional normalized contact email.

namestring | null

Optional display name.

avatar_urlHTTPS URL | null

Optional absolute HTTPS avatar URL.

metadataobject

Optional partner metadata object, up to 16 KB.

Request

ProvisionLocationGrantInput

location_idlocation UUID | location external ID

Required location in the selected project.

roleviewer | editor

Required role for this direct location grant. Roles may differ between locations.

Access validationWith neither scope selector, the identity must have active project access. With 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.
Object

EmbedSessionResponse

session.tokenstring

Opaque, short-lived credential for the embedded browser component. Treat it as a secret.

session.token_typeBearer

Authentication scheme used by the embed session token.

session.expires_atdatetime

Token expiration time in ISO 8601 UTC format.

session.expires_ininteger

Session lifetime in seconds.

session.project_iduuid

Compatibility alias for session.scope.project_id.

session.location_iduuid | null

Compatibility alias for session.scope.location_id.

session.location_scopefull | location | granted

Session scope mode. granted resolves the identity’s active direct location grants live.

session.external_user_idstring

Compatibility alias for session.identity.external_user_id.

session.identity.external_user_idstring

Partner identifier of the embedded user represented by the session.

session.scope.typegranted

Present for granted-location sessions.

session.scope.project_iduuid

Canonical Ceyo UUID of the project available to the session.

session.scope.location_iduuid | null

Canonical Ceyo UUID of the restricted location, or null for project scope.

session.scope.roleviewer | editor | null

Effective 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

Example requestcURL
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
}'
Example responseJSON
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 and retriesA unique 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
Errors

Error response envelope

errorError

Structured error payload.

Errors

Error

codestring

Stable snake_case code suitable for programmatic handling.

messagestring

Human-readable explanation of the failure.

detailsobject | array | null

Structured validation or request context when available.

request_idstring

Identifier to provide when requesting support.

Example error responseJSON
{
  "error": {
    "code": "invalid_request",
    "message": "The request could not be processed.",
    "details": {
      "field": "ttl_seconds"
    },
    "request_id": "req_01JEXAMPLE7J8Q2Y4K6M9"
  }
}
Errors

Status codes

400invalid_request

The JSON body is malformed, required fields are absent, or a request object or TTL is invalid.

401invalid_api_key

The platform API key is absent, invalid, expired, or revoked.

403forbidden | access_denied

The 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_found

The requested identity, project, or location was not found.

409idempotency_conflict

The idempotency key was reused with a different request.

422validation_failed

The inline identity, grant role, or access grant failed validation.

429rate_limit_exceeded

Too many requests were made; follow the Retry-After header.

POST
/embed/sessions/refresh

Atomically exchanges a current embed session token for a replacement with the same identity and scope.

Parameters

Request body

session_tokenstring

Current 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.

Required
ttl_secondsinteger

Lifetime of the replacement token in seconds. Minimum: 60. Maximum: 86400.

OptionalDefault: 3600
expires_ininteger

Compatibility alias for ttl_seconds. Do not send both unless their values are identical.

Optional
Scope is preservedRefresh accepts no identity, project, location, location scope, or role fields. The replacement inherits the current scope after the API revalidates access. A granted replacement continues to resolve direct location grants live. If required access was revoked, refresh fails; create a new session only after reconciling access.
Rotation and replay protectionA unique 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.
Object

EmbedSessionResponse

session.tokenstring

Opaque, short-lived credential for the embedded browser component. Treat it as a secret.

session.token_typeBearer

Authentication scheme used by the embed session token.

session.expires_atdatetime

Token expiration time in ISO 8601 UTC format.

session.expires_ininteger

Session lifetime in seconds.

session.project_iduuid

Compatibility alias for session.scope.project_id.

session.location_iduuid | null

Compatibility alias for session.scope.location_id.

session.location_scopefull | location | granted

Session scope mode. granted resolves the identity’s active direct location grants live.

session.external_user_idstring

Compatibility alias for session.identity.external_user_id.

session.identity.external_user_idstring

Partner identifier of the embedded user represented by the session.

session.scope.typegranted

Present for granted-location sessions.

session.scope.project_iduuid

Canonical Ceyo UUID of the project available to the session.

session.scope.location_iduuid | null

Canonical Ceyo UUID of the restricted location, or null for project scope.

session.scope.roleviewer | editor | null

Effective 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

Example requestcURL
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
}'
Example responseJSON
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
Errors

Error response envelope

errorError

Structured error payload.

Errors

Error

codestring

Stable snake_case code suitable for programmatic handling.

messagestring

Human-readable explanation of the failure.

detailsobject | array | null

Structured validation or request context when available.

request_idstring

Identifier to provide when requesting support.

Example error responseJSON
{
  "error": {
    "code": "invalid_request",
    "message": "The request could not be processed.",
    "details": {
      "field": "ttl_seconds"
    },
    "request_id": "req_01JEXAMPLE7J8Q2Y4K6M9"
  }
}
Errors

Status codes

400invalid_request

The JSON body is malformed, required fields are absent, or a request object or TTL is invalid.

401invalid_api_key | invalid_session_token

The API key or session token is invalid, revoked, expired, or outside the refresh window.

403session_workspace_mismatch | access_denied

The session belongs to another workspace or its identity no longer has access.

409idempotency_conflict

The idempotency key was reused with a different request.

429rate_limit_exceeded

Too many requests were made; follow the Retry-After header.

Security checklist

Authenticate your own userMint or refresh a session only after validating the user’s session in your application. Do not accept an arbitrary external_user_id from an unauthenticated browser.
Keep the platform key server-sideThe platform key can operate across its workspace. The browser receives only the short-lived, narrowly scoped embed token.
Protect session tokensSend tokens only over HTTPS, keep them out of URLs and analytics, and avoid persistent browser storage. Hold them in memory when possible.
Use short lifetimesThe default is 3600 seconds and the maximum is 86400 seconds. Refresh shortly before expiration or during the five-minute post-expiry exchange window.
Retry deliberatelyUse one idempotency key per intended create or refresh operation. Never retry a refresh with a new key after its outcome is unknown; first retry with the original key.