# Customer access

Source: https://ceyo.ai/docs/signal/customer-access-guides

### Customer access

Give each customer the correct project or location scope through an embedded Signal application or a short-lived link to hosted Signal.

### Choose embedded or hosted access

Both access modes use an embedded identity and its project or location grants. Choose where the customer should work.

**Embedded access**

Mount Signal inside your product with a short-lived embed session. Your backend mints the token, and the browser receives only that token. Embed roles are `viewer` or `editor`.

**Hosted access**

Create a redirect login link when the customer should open the hosted Signal application. The resource package must enable `pricing.frontend_delivery_enabled`.

**Origin configuration**

For embeds, add each HTTPS host-page origin to the issuing API key's `allowed_origins`. The same allowlist validates an optional hosted-login `return_url`.

> **Keep the API key server-side**
>
> Create embed sessions and login links from your authenticated backend. Never place a Signal API key in browser code or loader options.

### Provision identities and access grants

Use a workspace-scoped key with `identities:manage`. The external user ID is unique within the workspace, and repeating the request updates the same identity.

```curl
# Upsert an identity using your stable user ID.
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embedded-identities/customer-user-42' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{
    "email": "user-42@example.com",
    "name": "User 42"
  }'

# Grant access to one project.
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embedded-identities/customer-user-42/projects/customer-project/access' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{ "role": "viewer" }'
```

**Project grant**

Grants project access. It also applies when issuing a location-scoped session beneath that project.

**Location grant**

Use the nested `/projects/{project_id}/locations/{location_id}/access` path to restrict access to one location.

**Granted-location portfolio**

Mint with `location_scope: "granted"` and no `location_id` to expose active direct location grants in one project. Signal resolves them live, preserves each location's role, and ignores project grants for this scope. Revoking one grant removes only that location. The embed exposes Overview and Locations, not project-wide routes.

**Roles**

Grants accept `viewer`, `editor`, or `admin`. Embed sessions expose only viewer or editor; an admin grant resolves to editor in the iframe.

**Lifecycle**

Set an identity to `disabled` to block new access and revoke its active embed sessions. Delete a grant to remove that resource scope.

> **Optional inline embed provisioning**
>
> `POST /embed/sessions` can atomically upsert an identity, create a viewer or editor grant, and issue its first session through the `provision` object. With granted scope, use `provision.location_grants` to ensure one to 100 direct grants atomically. This requires a workspace-scoped key with both `embed_sessions:create` and `identities:manage`.

```json
{
  "external_user_id": "regional-manager-42",
  "project_id": "customer-project",
  "location_scope": "granted",
  "provision": {
    "location_grants": [
      { "location_id": "store-amsterdam", "role": "editor" },
      { "location_id": "store-utrecht", "role": "viewer" }
    ]
  }
}
```

### Create redirect login links

Use a key with `login_links:manage`. The identity must be active and already have access to the requested project or location, and the assigned package must enable customer access.

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/login-links' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{
    "external_user_id": "customer-user-42",
    "project_id": "customer-project",
    "expires_in": 900
  }'
```

**Lifetime**

`expires_in` defaults to 900 seconds and accepts 60 to 3,600 seconds.

**One-time use**

Send the returned `login_link.url` directly to the intended customer. Redemption marks it used, so it cannot be redeemed again.

**Destination**

Omit `redirect_path` to open the selected resource. Custom values must be safe paths within hosted Signal, not absolute URLs.

**Return URL**

An optional `return_url` must use HTTPS and an origin listed in the issuing API key's `allowed_origins`.

> **Inspect or revoke**
>
> Use `GET /login-links/{login_link_id}` to inspect status. Use `DELETE` on the same path to revoke a link while it is still pending. Treat the returned URL as a secret.

### Manage session renewal and revocation

Embed sessions default to 3,600 seconds and accept a TTL from 60 to 86,400 seconds. Refresh them through your backend; never expose the API key to the browser.

```curl
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: ${UNIQUE_REQUEST_ID}" \
  --data '{
    "session_token": "${CURRENT_SESSION_TOKEN}",
    "ttl_seconds": 3600
  }'
```

**Renew before expiry**

Implement the loader's `onTokenExpired` callback. Return the replacement token, or call `embed.updateToken()` after your backend refreshes it.

**Rotation**

A successful refresh exchanges the current token for a replacement. The exchanged token no longer authorizes embed API requests.

**Access checks**

Refresh and embed requests recheck the identity, resource, grant, role, and issuing API key. If refresh is rejected, mint a new session only after your application confirms the user still has access.

**Immediate revocation**

Disable the identity, revoke or change its grant, or revoke the issuing API key. Active embed requests then fail their live access check.

> **Use unique idempotency keys**
>
> Session create and refresh requests require an `Idempotency-Key`. Reuse it only when retrying the same operation with the same request body.
