> ## Documentation Index
> Fetch the complete documentation index at: https://cubed3-dependabot-js-alerts-table.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Generate Session

> HTTP reference for minting short-lived embed sessions that authorize external viewers under signed embedding without sharing API keys, and how to revoke one.

The Generate Session API provides secure, session-based authentication for [signed embedding][ref-signed-embedding]. This API creates temporary sessions that allow external users to access embedded dashboards and visualizations without exposing your API keys.

<Info>
  The Generate Session API is available on [Premium and Enterprise plans](https://cube.dev/pricing).
</Info>

## Authentication

The Generate Session API requires your [Cube Cloud API key][ref-api-keys] for authentication. The key must belong to an admin user: this endpoint mints a session for somebody else, choosing their identity and the groups and attributes their data access is resolved against, so a [personal key][ref-personal-api-keys] is rejected with `403 Forbidden`.

If the API key is [scoped to specific deployments][ref-api-keys], the `deploymentId` in the request body must be within the key's scope; otherwise the request is rejected with `403 Forbidden`. Unscoped keys can mint sessions for any deployment.

The deployment-scope restriction above is specific to Generate Session; see [Revoke a session](#revoke-a-session) for how that endpoint's authorization differs.

## Endpoint

```text theme={"dark"}
POST https://{accountName}.cubecloud.dev/api/v1/embed/generate-session
```

### Request Headers

| Header          | Value                  | Required |
| --------------- | ---------------------- | -------- |
| `Content-Type`  | `application/json`     | Yes      |
| `Authorization` | `Api-Key YOUR_API_KEY` | Yes      |

### Request Body

| Field                      | Type      | Required    | Description                                                                                                                                                                             |
| -------------------------- | --------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deploymentId`             | number    | Yes         | ID of the deployment the session should grant access to.                                                                                                                                |
| `externalId`               | string    | Conditional | Stable identifier for the external user. Provide either `externalId` or `internalId` (not both). Must be lowercase and trimmed.                                                         |
| `internalId`               | string    | Conditional | Username of an existing internal Cube Cloud user. Provide either `externalId` or `internalId` (not both). The user must already exist.                                                  |
| `email`                    | string    | No          | Email to attach to the provisioned external user. Used only with `externalId`.                                                                                                          |
| `userProfile`              | object    | No          | Display name and profile picture to attach to the external user. See [User profile](#user-profile). Not allowed with `internalId`.                                                      |
| `embedTenantName`          | string    | No          | Embed tenant to scope content to. Lowercase, 5–36 chars, must start with a letter and end with a letter or digit, only `a-z`, `0-9`, `-`. Defaults to the current tenant.               |
| `creatorMode`              | boolean   | No          | When `true`, mints a [creator-mode][ref-creator-mode] session and resolves groups/attributes against the embed tenant's scoped tables. Requires the `useCreatorMode` tenant flag.       |
| `userAttributes`           | array     | No          | Attribute values for row-level security. See [User attributes](#user-attributes). Not allowed with `internalId`.                                                                        |
| `groups`                   | string\[] | No          | Group memberships for the user. See [Groups](#groups). Not allowed with `internalId`.                                                                                                   |
| `userAttributeDefinitions` | array     | No          | Idempotently upsert attribute definitions before applying values. Requires `creatorMode: true`. See [Creator mode bootstrap](#creator-mode-bootstrapping-groups-and-user-attributes).   |
| `groupDefinitions`         | array     | No          | Idempotently upsert group definitions before assigning memberships. Requires `creatorMode: true`. See [Creator mode bootstrap](#creator-mode-bootstrapping-groups-and-user-attributes). |
| `securityContext`          | object    | No          | Custom security context object passed to Cube queries. Not allowed with `internalId`.                                                                                                   |
| `settings`                 | object    | No          | Per-session overrides for embed behavior, applied to every embed viewed with this session. See [Session settings](#session-settings).                                                   |
| `branchName`               | string    | No          | Deployment branch this session queries. Omit it to query production. See [Branch selection](#branch-selection).                                                                         |

<Info>
  When using `internalId`, the user must already exist in Cube Cloud. You cannot specify `groups`, `userAttributes`, `groupDefinitions`, `userAttributeDefinitions`, `securityContext`, or `userProfile` with `internalId` — the internal user's existing permissions are used instead.
</Info>

<Warning>
  Accounts are limited to 10,000 external users. To increase this limit, please contact support.
</Warning>

<h3 id="response">
  Response
</h3>

The API returns a session object:

```json theme={"dark"}
{
  "sessionId": "abc123def456..."
}
```

| Field       | Type   | Description                                            |
| ----------- | ------ | ------------------------------------------------------ |
| `sessionId` | string | Unique session identifier to use for embedding content |

Use the `sessionId` directly in your embed URL to authenticate and load content securely.
It is single-use and must be exchanged within 5 minutes; the token it's exchanged for is
then usable for about 23 hours — see [signed embedding][ref-signed-embedding] — unless
you revoke it sooner; see [Revoke a session](#revoke-a-session).

## Session settings

`settings` is an object of per-session overrides for embed behavior, applied to
every embed opened with this session. Omit a key to inherit what the layer below
says — the account-wide setting under **Embed → Settings** for the AI keys (except
`allowChatWorkspaceAuthoring`, which has none) and for `locale`/`timezone`, the
built-in default for the rest.

The switches are **tri-state**: `true`/`false` pins the behavior for this session,
taking precedence over whatever the layer below would have said. `locale` and
`timezone` carry a value rather than a switch, and sit one step below the
matching iframe URL parameter; a value Cube cannot use falls through instead of
failing the session.

The switches are signed into the session token, so a viewer cannot turn one back
on; use them for anything that differs per customer. (`locale` and `timezone`
grant nothing, and a URL parameter deliberately overrides them.) See [Show and hide
features](/embedding/iframe/feature-visibility) for how they relate to the
account-wide settings and the iframe URL parameters.

```json theme={"dark"}
{
  "settings": {
    "allowAi": false,
    "showWorkbookShare": false
  }
}
```

| Property                      | Type    | Required | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ----------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `allowAi`                     | boolean | No       | **Master switch** over every AI surface in the embed: the dashboard agent, the workbook chat, the AI summary widgets, and the standalone [embedded chat][ref-analytics-chat]. Omit to inherit the account-wide setting (allowed by default). Unlike the `show*` keys it is enforced server-side — a session with `false` cannot obtain AI Chat credentials at all, so no request made with it can consume AI tokens — and it outranks them: `false` here disables a surface whose own `show*` key is `true`. It does not change roles or permissions for non-AI API calls.                                |
| `showDashboardChat`           | boolean | No       | The AI chat (agent panel and launcher bubble) on embedded **published dashboards**. Omit to inherit the account-wide **Show AI chat on embedded dashboards** toggle (**Embed → Settings**; shown by default). Does not affect the standalone [embedded chat][ref-analytics-chat] surface.                                                                                                                                                                                                                                                                                                                 |
| `showWorkbookChat`            | boolean | No       | The [Creator Mode][ref-creator-mode] workbook's AI entry points: the chat side panel and its toggle, the launchpad's **Ask Cube Agent** button, and **Fix in chat** on a failed report. Omit to inherit the account-wide toggle (shown by default).                                                                                                                                                                                                                                                                                                                                                       |
| `showAiWidgets`               | boolean | No       | Authoring and refreshing AI summary widgets: the **AI widget** entry in the dashboard editor's Add widgets menu, and a widget's prompt input plus Generate/Regenerate controls. An already-generated summary still renders, so a published dashboard keeps its content — it simply cannot be regenerated. Omit to inherit the account-wide toggle (enabled by default).                                                                                                                                                                                                                                   |
| `allowChatWorkspaceAuthoring` | boolean | No       | Whether AI Chat authenticated with this session may create or modify persistent workspace content. `false` keeps ad-hoc analysis and inline tables/charts, but the agent cannot save or update standalone explorations and reports, create or modify workbooks, create or publish dashboards, or direct users to those surfaces — intended for headless chat integrations that render answers in their own UI. Omit or `true` preserves the user's role-derived capabilities and never grants more. Changes AI Chat tools and instructions only.                                                          |
| `showWorkbookShare`           | boolean | No       | Sharing in the [Creator Mode][ref-creator-mode] workspace: the workbook header's **Share** button and the **Share** action on a workbook, dashboard, exploration, or folder row. Set `false` for a deployment where each embed user should only ever see their own content. Shown by default. Hides the entry points only — it does not revoke access already granted, and never widens what the session may do.                                                                                                                                                                                          |
| `showDashboardSettings`       | boolean | No       | The dashboard editor's settings panel — the gear button and the sidebar behind it. That panel is the only place an embed user reaches the dashboard slug, the per-dashboard time zone, the per-dashboard agent, the grid behavior switches, and the dashboard theme, so `false` withdraws all of them together. Shown by default.                                                                                                                                                                                                                                                                         |
| `showMemberNames`             | boolean | No       | The **Show Member Names** item in the workbook data pane's more-actions menu. Member names are the data model's own identifiers, so this is schema protection in the same family as **Show Generated SQL**. `false` hides the item **and** pins the pane to titles, returning a user who had already switched to names. Shown by default.                                                                                                                                                                                                                                                                 |
| `showGroupByCube`             | boolean | No       | The **Group by Cube** / **Group by Folder** item in the data pane's more-actions menu. `false` hides the item **and** pins the pane to the grouping it would have defaulted to — by folder when the view defines folders, by cube otherwise. No content is withdrawn either way. Shown by default.                                                                                                                                                                                                                                                                                                        |
| `locale`                      | string  | No       | UI language for every embed opened with this session, as a BCP-47 code — a full code (`es-ES`), a short code (`es`), or a regional variant all resolve to a shipped language. Sits below a runtime [`cube:action:set-locale`](/embedding/iframe/events#cube-action-set-locale) message and the per-iframe `?locale=` parameter, both of which still win, and above the account-wide default under **Embed → Settings**. A language Cube does not ship falls through to that default rather than failing the session. See [Localization](/embedding/iframe/localization).                                  |
| `timezone`                    | string  | No       | IANA time zone the agent runs its queries in for every embed opened with this session (e.g. `America/New_York`). Below a runtime [`cube:action:set-timezone`](/embedding/iframe/events#cube-action-set-timezone) message and the per-iframe `?timezone=` parameter, and above the account-wide default — and above a zone a dashboard's author pinned, so every dashboard this session opens is bucketed in it. Stays subject to the account's user-time-zone policy; a bare UTC offset or an unknown name falls through rather than failing the session. See [Time zones](/embedding/iframe/time-zones). |

<Note>
  Chrome that belongs to a **placement** rather than to a viewer — the dashboard
  header and its controls, the workbook back button, chart export — is set with URL
  parameters on the iframe `src` instead, and is not accepted in this `settings`
  object. [Show and hide features](/embedding/iframe/feature-visibility) covers
  both layers together and the rule for choosing between them.
</Note>

<Note>
  The [Chat API](/reference/embed-apis/chat-api) has no `settings` object of its
  own. Of the keys above, its `sessionSettings` object accepts
  `allowChatWorkspaceAuthoring` and `timezone`, which mean the same thing there as
  `settings.allowChatWorkspaceAuthoring` and `settings.timezone` do here — except
  `sessionSettings.timezone` has no `cube:action:set-timezone` message, `?timezone=`
  parameter, or dashboard-pinned zone above it to lose to; an unusable value still
  falls through to the deployment default rather than failing the session.
</Note>

## Branch selection

Omit `branchName`, or pass the deployment's production branch, and the session queries
production — the default.

Pass any other branch and every Cube query the session makes — from embedded
dashboards, the [Creator Mode][ref-creator-mode] workspace, and chat alike — runs
against that branch's data model instead, which is how an embed previews data model
changes before they ship.

The branch is resolved when the session is generated, not when the iframe loads, so a
bad branch fails the API call rather than the embed:

* a name that isn't a branch of the deployment returns `404`;
* a branch nothing is serving returns `400`. A branch is served while someone has it
  open in Cube, or permanently once its [staging
  environment][ref-environments-staging] is enabled.

Content itself isn't branch-scoped: the same saved dashboards and workbooks render
whichever branch the session selects.

## User profile

`userProfile` lets you attach a human-readable display name and avatar to an external user so they render with a recognizable identity inside embedded surfaces (workbook owners, dashboard headers, etc.) instead of the raw `externalId`.

```json theme={"dark"}
{
  "userProfile": {
    "displayName": "Jane Query",
    "picture": "https://example.com/avatars/jq.png"
  }
}
```

| Property      | Type   | Required | Notes                                                                                                   |
| ------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------- |
| `displayName` | string | No       | Human-readable name shown next to the user's avatar.                                                    |
| `picture`     | string | No       | Publicly accessible URL of the user's profile picture. Must be an absolute `http://` or `https://` URL. |

**Behavior**:

* Both fields are persisted on the external user (keyed by `externalId`).
* Sending `userProfile` on subsequent `generate-session` calls overwrites the previously-saved values.
* Omitting `userProfile` (or omitting a property inside it) preserves whatever was saved before.

### Picture format

The `picture` URL must be reachable by the end-user's browser — it's loaded directly via an `<img>` tag in the embedded UI, not proxied through Cube Cloud. The server validates only that the value is a syntactically valid `http(s)://` URL; it does not download or sniff the content.

Use a URL that returns one of the common web image formats: **PNG, JPEG, GIF, WebP, or SVG**. Other content types (videos, PDFs, HTML pages) will fail to render and the avatar will fall back to the user's initials.

Practical guidance:

* Prefer HTTPS URLs — mixed-content rules will block `http://` images on HTTPS embed pages.
* Keep the image under \~1 MB and ideally square (e.g. 96×96 or 256×256). Avatars are displayed in small containers, so anything larger is wasted bandwidth.
* The URL must be publicly reachable — signed URLs that expire or assets behind auth headers will not load.
* If the URL fails to load for any reason (404, wrong content type, CORS, network error), the UI gracefully falls back to an initials avatar derived from `displayName`. No error is returned to the caller.

## User attributes

`userAttributes` is an array of `{ name, value }` pairs that drive row-level security in queries.

```json theme={"dark"}
{
  "userAttributes": [
    { "name": "department", "value": "Sales" },
    { "name": "tier", "value": 2 },
    { "name": "regions", "value": ["us-east", "eu-west"] },
    { "name": "thresholds", "value": [10, 25, 50] }
  ]
}
```

| Property | Type                                                       | Notes                                                                       |
| -------- | ---------------------------------------------------------- | --------------------------------------------------------------------------- |
| `name`   | string                                                     | Must reference an existing attribute definition (see lookup rules below).   |
| `value`  | `string` \| `number` \| `string[]` \| `number[]` \| `null` | The value type must match the definition's `type`. `null` clears the value. |

**Attribute definition lookup**:

* **Read-only mode** (`creatorMode` omitted or `false`): names are resolved against the tenant-wide attribute catalog (managed in **Settings → User Attributes** or via the admin GraphQL API). Any name not present there fails with `User attributes not found`.
* **Creator mode** (`creatorMode: true`): names are resolved against the embed tenant's scoped catalog (`embed_user_attributes`). Use `userAttributeDefinitions` in the same request to upsert definitions on the fly — see [Creator mode bootstrap](#creator-mode-bootstrapping-groups-and-user-attributes).

**Rules**:

* Duplicate `name` entries are rejected with `400 Bad Request`.
* Values are persisted per user. Subsequent calls with the same `externalId` overwrite previous values for the supplied names.

## Groups

`groups` is an array of group **names** (not IDs) that the user should belong to. Group definitions must already exist (or be created in the same request via `groupDefinitions` in creator mode).

```json theme={"dark"}
{ "groups": ["analysts", "marketing"] }
```

**Behavior**:

| Value                       | Effect                                                    |
| --------------------------- | --------------------------------------------------------- |
| Field omitted (`undefined`) | Existing memberships are preserved.                       |
| `[]` (empty array)          | All memberships are cleared.                              |
| Populated array             | Memberships are replaced with exactly the supplied names. |

**Group definition lookup**:

* **Read-only mode**: names are resolved against tenant-wide groups (managed in **Settings → Groups** or via the admin GraphQL API). The membership row references the global group.
* **Creator mode**: names are resolved against the embed tenant's scoped groups (`embed_user_groups`). Use `groupDefinitions` in the same request to upsert them.

If any name in `groups` cannot be resolved, the request fails with `Groups with names <missing> not found`.

## Creator mode: bootstrapping groups and user attributes

In API-first integrations you often want to mint an embed session and define the groups/attributes it references in a single call, without first making a round trip to the admin UI. The `groupDefinitions` and `userAttributeDefinitions` fields do that — they idempotently upsert definitions in the embed tenant's scoped tables and are validated **before** `groups` and `userAttributes` are applied.

Both fields:

* Require `creatorMode: true`.
* Require an `embedTenantName` (definitions are only meaningful inside an embed tenant).
* Require the `useCreatorMode` tenant flag — contact support to enable.
* Land in the embed-tenant scope only — they never modify tenant-wide groups or attributes.
* Are idempotent: running the same request twice produces the same end state.

### `groupDefinitions`

```json theme={"dark"}
{
  "groupDefinitions": [
    { "name": "analysts", "description": "Read-only viewers" },
    { "name": "marketing" }
  ]
}
```

| Property      | Type   | Required | Notes                                                                     |
| ------------- | ------ | -------- | ------------------------------------------------------------------------- |
| `name`        | string | Yes      | Group name. Existing groups with this name are reused.                    |
| `description` | string | No       | Updated when supplied and different from the stored value. Never cleared. |

Duplicate `name` entries within the same request are rejected.

### `userAttributeDefinitions`

```json theme={"dark"}
{
  "userAttributeDefinitions": [
    {
      "name": "department",
      "type": "string",
      "displayName": "Department",
      "defaultValue": "Unassigned",
      "description": "Org unit"
    }
  ]
}
```

| Property       | Type   | Required | Notes                                                                                 |
| -------------- | ------ | -------- | ------------------------------------------------------------------------------------- |
| `name`         | string | Yes      | Attribute name. Existing attributes with this name are reused.                        |
| `type`         | enum   | Yes      | One of `string`, `number`, `string_array`, `number_array`. **Immutable** — see below. |
| `displayName`  | string | No       | Updated when supplied and different from the stored value.                            |
| `defaultValue` | string | No       | Updated when supplied and different from the stored value.                            |
| `description`  | string | No       | Updated when supplied and different from the stored value.                            |

**`type` is immutable.** If a definition with the supplied `name` already exists with a different `type`, the request fails with `cannot change type` and nothing is upserted. This protects every value already stored against that attribute from silently becoming invalid. To change the type, delete the attribute via the [embed-tenant admin API](#embed-tenant-admin-api) and recreate it.

Duplicate `name` entries within the same request are rejected.

### Bootstrap example

Define a group and an attribute, assign the user to both, and mint a session — all in one call:

```javascript theme={"dark"}
const session = await fetch(
  `https://${ACCOUNT_NAME}.cubecloud.dev/api/v1/embed/generate-session`,
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Api-Key ${API_KEY}`,
    },
    body: JSON.stringify({
      deploymentId: DEPLOYMENT_ID,
      externalId: "user-123",
      embedTenantName: "acme-corp",
      creatorMode: true,

      // Upserted before validation runs
      groupDefinitions: [
        { name: "analysts", description: "Read-only viewers" },
      ],
      userAttributeDefinitions: [
        { name: "department", type: "string", displayName: "Department" },
      ],

      // Reference the names we just defined
      groups: ["analysts"],
      userAttributes: [{ name: "department", value: "Sales" }],
    }),
  }
);
```

A second call with the same body produces the same end state: the group and attribute already exist, descriptions/display names are reconciled if they changed, and the user's memberships and values are re-applied.

## Embed-tenant admin API

To list or delete the groups and attributes that have been bootstrapped into an embed tenant, use the admin endpoints scoped to that tenant:

```text theme={"dark"}
GET    /api/v1/embed-tenants/{embedTenantName}/groups
DELETE /api/v1/embed-tenants/{embedTenantName}/groups/{id}
GET    /api/v1/embed-tenants/{embedTenantName}/user-attributes
DELETE /api/v1/embed-tenants/{embedTenantName}/user-attributes/{id}
```

These endpoints use the same `Api-Key` authentication as Generate Session and require admin access. List endpoints return cursor-paginated results (`?first=`, `?after=`).

## Code Example

<CodeGroup>
  ```python title="Python" Python theme={"dark"}
  import requests

  API_KEY = 'YOUR_API_KEY'
  ACCOUNT_NAME = 'your-account'

  # Generate a session on your server
  response = requests.post(
      f'https://{ACCOUNT_NAME}.cubecloud.dev/api/v1/embed/generate-session',
      headers={
          'Content-Type': 'application/json',
          'Authorization': f'Api-Key {API_KEY}'
      },
      json={
          'deploymentId': 32,
          'externalId': 'user@example.com',
          'userAttributes': [
              {'name': 'department', 'value': 'Sales'}
          ],
          'groups': ['analysts']
      }
  )

  session_id = response.json()['sessionId']
  ```

  ```javascript title="JavaScript" JavaScript theme={"dark"}
  const API_KEY = "YOUR_API_KEY";
  const ACCOUNT_NAME = "your-account";

  // Generate a session on your server
  const response = await fetch(
    `https://${ACCOUNT_NAME}.cubecloud.dev/api/v1/embed/generate-session`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Api-Key ${API_KEY}`,
      },
      body: JSON.stringify({
        deploymentId: 32,
        externalId: "user@example.com",
        userAttributes: [{ name: "department", value: "Sales" }],
        groups: ["analysts"],
      }),
    }
  );

  const { sessionId } = await response.json();
  ```

  ```bash title="Bash" cURL theme={"dark"}
  curl -X POST "https://your-account.cubecloud.dev/api/v1/embed/generate-session" \
    -H "Content-Type: application/json" \
    -H "Authorization: Api-Key YOUR_API_KEY" \
    -d '{
      "deploymentId": 32,
      "externalId": "user@example.com",
      "userAttributes": [
        {"name": "department", "value": "Sales"}
      ],
      "groups": ["analysts"]
    }'
  ```
</CodeGroup>

Use session ID in [signed embedding][ref-signed-embedding].

## Revoke a session

```text theme={"dark"}
POST https://{accountName}.cubecloud.dev/api/v1/embed/session/revoke
```

Call this from your application's logout handler to end an embed session from the
server side. Pass the `sessionId` that Generate Session returned, and still remove the
embedded iframe on logout — revoking a session doesn't tear it down on its own.

```bash theme={"dark"}
curl -X POST "https://your-account.cubecloud.dev/api/v1/embed/session/revoke" \
  -H "Content-Type: application/json" \
  -H "Authorization: Api-Key YOUR_API_KEY" \
  -d '{ "sessionId": "abc123def456..." }'
```

Returns `204 No Content` on success, with no body.

It requires an admin principal — an API key, embed JWT, or bearer token belonging
to an admin user.

<Warning>
  Unlike Generate Session, an API key's scope is **not** checked: the request body
  carries only a `sessionId`, so a key scoped to one deployment can revoke a session
  belonging to any deployment in the account.
</Warning>

Only tokens issued after this endpoint shipped carry the claim revocation checks
against — an older token keeps working until its own signed expiry, 24 hours from
mint (not the \~23-hour figure above, which is when the iframe itself stops
trusting a token early). The `204` response is identical either way.

Revocation covers the embed session only. A Cube API token the embed obtained from
`POST /api/v1/deployments/{deploymentId}/token` is a separate credential and keeps
working — revoke or expire it separately if your logout has to stop data-API access.

See [Revoke an Embed Session][ref-revoke-session] for the full request and response
reference, including the `204` idempotency behavior and the `403` when embedding isn't
enabled for the account.

[ref-api-keys]: /admin/account-billing/api-keys

[ref-revoke-session]: /api-reference/embed/revoke-an-embed-session

[ref-personal-api-keys]: /admin/account-billing/api-keys#personal-api-keys

[ref-signed-embedding]: /embedding/iframe/auth/signed

[ref-creator-mode]: /embedding/iframe/creator-mode

[ref-analytics-chat]: /embedding/iframe/analytics-chat

[ref-environments-staging]: /admin/deployment/environments#staging-environments
