# KViews

> Render JSX inside the Kustomer timeline and insights panel, embed hosted UI with DynamicCard and the Cards SDK, and call external APIs safely with commands.

Source: https://developer.kustomer.com/docs/apps/kviews

A **KView** (Klass View) is a JSX template that Kustomer renders inside the agent desktop. Kustomer hosts and evaluates the template, so a KView needs no server: it reads the record being viewed, formats it with Kustomer's own components, and can call the Kustomer API or your app's commands when the agent clicks something. When you need more than a template can do, a KView can embed a page you host and talk to it through the Cards SDK.

## Define a KView

```json title="kviews (excerpt)"
{
  "kviews": [
    {
      "name": "kustomer.app.acme_orders.recent-orders",
      "resource": "customer",
      "context": "smartbar-card",
      "meta": { "displayName": "Acme orders", "icon": "cart", "state": "open" },
      "template": "<Segment>\n  <BasicRow label=\"Lifetime value\" value={_.get(customer, 'custom.@acmeordLifetimeValueNum')} />\n  <BasicRow label=\"Tier\" value={_.get(customer, 'custom.@acmeordLoyaltyTierStr', 'none')} />\n</Segment>"
    }
  ]
}
```

**KView**

## Where KViews appear

| `context`           | Renders                                                        | Allowed `resource`            |
| ------------------- | -------------------------------------------------------------- | ----------------------------- |
| `smartbar-card`     | A card in the insights panel on the right of the timeline      | Any                           |
| `smartbar-details`  | The details panel at the top of the insights panel             | Only a Klass your app defines |
| `expanded-timeline` | The body of a KObject when an agent expands it in the timeline | Only a Klass your app defines |

> **Don't take over shared views:** `smartbar-details` and `expanded-timeline` replace the org's own layout for that resource. Use them only for your app's Klasses; on `customer` or `conversation` they would conflict with the org's settings.

_Figure: The Kustomer agent desktop with a customer timeline in the center. On the right, the insights panel shows the details panel at the top and an 'Acme orders' card below it. In the timeline, an expanded Acme order shows line items rendered by an expanded-timeline KView. Where each KView context renders._

## Write the template

A template is JSX evaluated in the browser. It can start with JavaScript (constants, helper functions, even React components) and must end with a single JSX expression:

```jsx title="kviews/order-timeline.jsx"
const money = (n) => (n == null ? '–' : `$${Number(n).toFixed(2)}`)

const statusColor = {
  shipped: 'var(--text-primary)',
  delayed: 'var(--alert-text)',
  refunded: 'var(--text-secondary)',
}

;<Segment>
  <Grid>
    <Column size="eight">
      <BasicField label="Order" value={object.custom.orderNumberStr} />
      <BasicField label="Placed" value={moment(object.custom.placedAt).format('LL')} />
    </Column>
    <Column size="eight">
      <BasicField label="Total" value={money(object.custom.totalNum)} />
      <div style={{ color: statusColor[object.custom.statusStr] }}>{_.capitalize(object.custom.statusStr)}</div>
    </Column>
  </Grid>
  {object.custom.trackingUrl && (
    <Button size="small" link={object.custom.trackingUrl}>
      Track package
    </Button>
  )}
</Segment>
```

Remember JSX isn't HTML: use `className`, `style={{ … }}` objects, and close every tag. To try a template before you save it, use the [KView playground](https://developer.kustomer.com/docs/apps/kview-components#try-it).

### What's in scope

| Name           | Contains                                                                                                                                                     |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `object`       | The record the view renders. For a KObject: `id`, `custom`, `data`, `title`, `description`, `externalId` and so on.                                          |
| `customer`     | The customer, flattened: `id` plus its attributes (`customer.name`, `customer.emails`, `customer.custom`). Relationships such as `company` become plain IDs. |
| `conversation` | The conversation's attributes, when one is open                                                                                                              |
| `company`      | The company's attributes, when there is one                                                                                                                  |
| `currentUser`  | The signed-in agent: `id`, their attributes, and `teams`                                                                                                     |
| `appSettings`  | Your app's settings for this org. Secret settings aren't readable here.                                                                                      |
| `context`      | The context object passed to embedded cards                                                                                                                  |
| `type`         | The record type, such as `customer` or `kobject.order`                                                                                                       |
| `data`         | All of the above as one object, for helpers such as `renderHandlebars`                                                                                       |
| `_`            | [lodash](https://lodash.com/docs)                                                                                                                            |
| `moment`       | [Moment.js](https://momentjs.com/docs/) for dates                                                                                                            |
| `React`        | React, for components with state                                                                                                                             |

Attributes can be missing, so read them with `_.get(customer, 'custom.loyaltyTierStr', 'none')` rather than chaining property access.

### Components

Kustomer provides components that match its UI and respect dark mode and data masking. [KView components](https://developer.kustomer.com/docs/apps/kview-components) previews each one and lists every prop.

| Group              | Components                                                                                                                                                 | Use                                        |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| Layout             | `Container` (`<Container usePadding>` adds standard padding), `Segment`, `Grid`, `Column` (`size="one"` to `"sixteen"`, adding up to sixteen)              | Structure and padding                      |
| Timeline fields    | `BasicField` (label and value), `Field` (label from metadata, typed formatting), `Images`                                                                  | `expanded-timeline` views and cards        |
| Detail rows        | `BasicRow`, `Row` (metadata-aware, inline editable), `RowAssignmentUser`, `RowAssignmentTeam`, `RowPriority`, `CreateFormRow`                              | `smartbar-details`, cards and create forms |
| Relationship cards | `Customer`, `Company`, `Conversation`, `Kobject`, and `CustomerField`, `CompanyField`, `ConversationField`, `KobjectField` with a label                    | Link to related records                    |
| Buttons and pills  | `Button`, `ButtonSecondary`, `ButtonDanger`, `ButtonText`, `ButtonGroup` (`size`: `xSmall` or `small`; `onClick` or `link`), `Pill` (`type` and `message`) | Actions and status                         |
| Data               | `WithUser` (render prop with a user by ID)                                                                                                                 | Show an assignee's name                    |
| Embedding          | `Card`, `DynamicCard`                                                                                                                                      | Your hosted UI, see below                  |

`CollapsedRows`, from older generated layouts, is deprecated: it renders its children and no longer collapses them.

`Row` writes back to the record when the agent edits it inline:

```jsx
<Row field="statusStr" value={object.custom.statusStr} object="kobject" editable componentAttributes={{ fullWidth: true }} />
```

### Call the Kustomer API

`KustomerRequest` calls the API as the signed-in agent, with their permissions. Return a string from the optional callback to show it as a toast:

```jsx
<Button
  size="small"
  onClick={() =>
    KustomerRequest(
      { url: `/v1/conversations/${conversation.id}`, method: 'PATCH', body: { tags: ['acme-refund'] } },
      (err) => (err ? 'Could not tag the conversation' : 'Tagged for refund'),
    )
  }
>
  Mark for refund
</Button>
```

### Handle masked data

If the org uses data masking and the agent can't see a field, the template receives `{ isMasked: true, maskedPresentation: "••••@example.com" }` instead of the value. Kustomer's field components handle this for you. In your own markup, check before using the value:

```jsx
const email = _.get(customer, 'emails[0].email')

;<div>
  {email?.isMasked ? (
    <Button size="small" disabled>
      Email hidden ({email.maskedPresentation})
    </Button>
  ) : (
    <Button size="small" link={`https://admin.acme.example/customers?email=${encodeURIComponent(email)}`}>
      Open in Acme
    </Button>
  )}
</div>
```

### Support dark mode

Agents switch between light and dark themes, and Kustomer's components follow along. For your own markup, use Kustomer's theme variables instead of fixed colors, sizes and fonts:

```jsx
<div
  style={{
    background: 'var(--background-secondary)',
    color: 'var(--text-primary)',
    border: '1px solid var(--border-primary)',
    borderRadius: 'var(--border-radius-primary)',
    fontFamily: 'var(--font-family)',
    padding: 8,
  }}
>
  <div style={{ fontWeight: 'var(--font-weight-medium)' }}>Acme loyalty</div>
  <div style={{ color: 'var(--text-secondary)' }}>Gold member since 2024</div>
</div>
```

Templates get the variables from the agent desktop. Pages in a `Card` or `DynamicCard` get them from the Cards SDK, which sets each one, and `color-scheme`, on the page's root element, and updates them when the agent switches theme (listen with `Kustomer.on('theme', …)` to re-render). The [playground](https://developer.kustomer.com/docs/apps/kview-components#try-it) previews both themes.

| Purpose           | Variables                                                                                                                                                                                                                                              |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Backgrounds       | `--background-primary`, `--background-secondary` and `--background-tertiary`, each with a `-hover` variant; `--background-primary-active`, `--background-primary-inverse`                                                                              |
| Text              | `--text-primary`, `--text-secondary`, `--text-secondary-hover`, `--text-hint`, `--text-link`, `--text-link-hover`, `--text-info`, `--text-primary-inverse`                                                                                             |
| Borders and forms | `--border-primary`; `--form-border` with `-hover`, `-active` and `-disabled`; `--form-background-disabled`, `--form-text-disabled`, `--form-text-placeholder`                                                                                          |
| Icons             | `--icon-primary`, `--icon-primary-disabled`, `--icon-active`                                                                                                                                                                                           |
| Status            | `--info-text`, `--info-background` and `--info-border`, and the same for `success`, `alert` and `danger`; `--info-secondary-text` and `--info-secondary-background`; `--neutral-text` and `--neutral-background`, with `--neutral-secondary-` variants |
| Buttons           | `--button-primary-background` and `--button-primary-text`; `--button-secondary-text` and `--button-secondary-border`; `--button-danger-text` and `--button-danger-border`; `--button-subtle-text`; each with `-hover` and `-disabled` variants         |
| Overlays          | `--tooltip-text`, `--tooltip-background`, `--modal-overlay-background`                                                                                                                                                                                 |
| AI                | `--background-ai`, `--background-ai-subtle`, `--border-ai`, `--text-ai`, `--icon-ai`. These are gradients: use them as a `background`.                                                                                                                 |
| Corner radius     | `--border-radius-sharp` (4px), `--border-radius-primary` (8px), `--border-radius-large` (12px)                                                                                                                                                         |
| Typography        | `--font-family` (Inter), `--font-size` (14px), and `--font-weight-light`, `-normal`, `-medium`, `-semiBold` and `-bold` (300 to 700)                                                                                                                   |
| Control heights   | `--komponent-size-x-small` (26px), `--komponent-size-small` (30px), `--komponent-size-medium` (36px)                                                                                                                                                   |

#### Older variable names

Templates written before these variables existed use older names. Most are still set, so those templates keep working, but they're deprecated and don't always match the theme: `--font-secondary-color`, for example, is the same gray in light and dark mode. Replace them as you update a template.

| Older name                                                 | Use instead                                                          | Status        |
| ---------------------------------------------------------- | -------------------------------------------------------------------- | ------------- |
| `--background-primary-color`                               | `--background-primary`                                               | Deprecated    |
| `--background-primary-color-hover`                         | `--background-primary-hover`                                         | Deprecated    |
| `--background-primary-color-selected`                      | `--background-primary-active`                                        | Deprecated    |
| `--background-primary-color-rgb`                           | `--background-primary`, with `color-mix()` for transparency          | No longer set |
| `--background-primary-color-inverse-rgb`                   | `--background-primary-inverse`                                       | No longer set |
| `--background-secondary-color`                             | `--background-secondary`                                             | Deprecated    |
| `--background-tertiary-color`                              | `--background-tertiary`                                              | Deprecated    |
| `--background-tertiary-color-hover`                        | `--background-tertiary-hover`                                        | Deprecated    |
| `--background-tertiary-color-rgb`                          | `--background-tertiary`, with `color-mix()`                          | No longer set |
| `--background-tertiary-color-hover-rgb`                    | `--background-tertiary-hover`, with `color-mix()`                    | No longer set |
| `--background-nav-primary-color`                           | None. It's the color of Kustomer's navigation.                       | Deprecated    |
| `--background-modal-overlay-color-rgb`                     | `--modal-overlay-background`                                         | No longer set |
| `--background-search-results-has-active-users-color`       | None                                                                 | No longer set |
| `--background-search-results-has-active-users-color-hover` | None                                                                 | No longer set |
| `--font-primary-color`                                     | `--text-primary`                                                     | Deprecated    |
| `--font-primary-color-inverse`                             | `--text-primary-inverse`                                             | Deprecated    |
| `--font-secondary-color`                                   | `--text-secondary`                                                   | Deprecated    |
| `--font-secondary-hover-color`                             | `--text-secondary-hover`                                             | Deprecated    |
| `--icon-primary-color`                                     | `--icon-primary`                                                     | Deprecated    |
| `--icon-primary-color-disabled`                            | `--icon-primary-disabled`                                            | Deprecated    |
| `--form-border-color`                                      | `--form-border`                                                      | Deprecated    |
| `--form-border-color-hover`                                | `--form-border-hover`                                                | Deprecated    |
| `--form-border-color-disabled`                             | `--form-border-disabled`                                             | Deprecated    |
| `--color-hint`                                             | `--background-secondary`. Despite the name, it's a background color. | Deprecated    |

## Call APIs with commands

A **command** is a named HTTP request defined in your app and executed by Kustomer's servers. Your template or iframe runs it by name. Kustomer fills in secret settings or the app's OAuth token, so credentials never reach the agent's browser.

```json title="commands (excerpt)"
{
  "commands": [
    {
      "name": "acme_orders.app.lookup-order",
      "displayName": "Look up an Acme order",
      "url": "https://{{{storeDomain}}}/api/orders/{{{orderId}}}",
      "httpMethod": "get",
      "permittedArgs": ["storeDomain", "orderId"],
      "appSettings": { "apiKey": { "key": "acme_orders.default.apiKey" } },
      "defaultHeaders": { "Authorization": "Bearer {{{apiKey}}}" },
      "cacheSeconds": 60,
      "outputSchema": {
        "type": "object",
        "properties": { "status": { "type": "string" }, "items": { "type": "array" } },
        "additionalProperties": true
      }
    }
  ]
}
```

**Command**

The app definition is rejected if a command has any other field.

> **Never pass secrets as args:** `args` come from the agent's browser, which can read and change them. Put API keys in a `secret` setting and reference it through `appSettings`.

### Run a command

From a template, call `runCommand`. From an iframe, call `Kustomer.command.run`. Both go through the same endpoint, which you can also call directly:

`POST /v1/commands/{name}/run`

```json title="Request body"
{
  "args": { "storeDomain": "shop.acme.example", "orderId": "4417" }
}
```

```js title="Cards SDK"
const order = await Kustomer.command.run('acme_orders.app.lookup-order', {
  args: { storeDomain: 'shop.acme.example', orderId: '4417' },
})
```

The request body can also have `headers` and `body`. Both are templates too, so a caller can send `{ "Authorization": "Bearer {{{apiKey}}}" }` and the browser only ever sends the placeholder; `{{{apiKey}}}` is filled in on the server from the `appSettings` mapping.

How templates are filled in:

- **Use triple braces.** `{{apiKey}}` HTML-escapes the value, so `=` becomes `&#x3D;` and `&` becomes `&amp;`. `{{{apiKey}}}` inserts it as is.
- **A string that is exactly `"/#name"`** inserts the value with its type, such as a number or boolean, instead of text. A falsy value (`0`, `false`, `""`) becomes `null`.
- **Args win over settings.** An arg with the same name as a setting replaces it.
- **`inputSchema` casts the body before templates are filled in.** A property typed `number` that holds `"/#amount"` becomes `NaN`, sent as `null`. Leave templated properties untyped.

What your API receives:

- A JSON request (`Content-Type: application/json`) with the rendered URL, headers and body.
- `x-kustomer-org-id` and `x-kustomer-user-id` headers identifying the org and the agent who ran the command. Kustomer sets them after your headers, so a caller can't change them.
- With `appAccountName`, the OAuth token in `Authorization: Bearer …`, or in the auth config's `customHeader`.

Kustomer returns your response, filtered through `outputSchema`, as the command's result. A run fails with `400` before your API is called if it passes an arg that isn't in `permittedArgs` (`cannot run a command with unpermitted args provided in request body`), or if the body misses an `inputSchema` `required` property (`missing required params for inputSchema`) or has one it forbids (`unsupported property provided for inputSchema`).

_A command builder. Edit a command definition as JSON and a run request with args, headers and body, and fill in the org's setting values, marking each as secret or not. The result shows either why the app definition is rejected (unknown fields such as headers instead of defaultHeaders, uppercase httpMethod, arg names shorter than 3 characters), the 400 error a run returns (unpermitted args, a missing required or forbidden inputSchema property), or the exact request your API receives: method, rendered URL, headers (Content-Type, defaultHeaders, request headers, x-kustomer-org-id, x-kustomer-user-id and an OAuth token) and the cast body, with each inserted value colored by where it came from and secrets marked. Examples cover an order lookup, Basic auth with base64Encode, a POST body cast by inputSchema and double braces HTML-escaping a query and an API key. Notes explain empty values, escaping, args replacing settings and the body replacing defaultBody._

### Caching and cache fallback

With `cacheSeconds` set, Kustomer stores every response for 24 hours:

- A cached response younger than `cacheSeconds` is returned without calling your API.
- If your API returns a `4xx` or `5xx`, **cache fallback mode** turns on for that combination of org, command, URL, headers and body. Kustomer returns the cached response even when it's stale, and retries your API on every fifth run.
- The first successful (`< 400`) response turns fallback mode off.

This keeps cards working through a rate limit or an outage on your side.

## Embed your own UI

When a template isn't enough (you need your own frontend, a build step, or server-side code) render an iframe with `DynamicCard`. The page you host loads the Cards SDK to receive context and talk to Kustomer.

```jsx title="template"
<DynamicCard src={`https://kustomer.acme.example/card?customer=${customer.id}`} context={context} />
```

| Component     | Size                                                  |
| ------------- | ----------------------------------------------------- |
| `DynamicCard` | Resizes to fit as your page calls `Kustomer.resize()` |
| `Card`        | Fixed: set `height` (for example `"320px"`)           |

Pass a custom `context` object to send your own data instead of the default:

```jsx
<DynamicCard src="https://kustomer.acme.example/card" context={{ type: 'acmeOrder', customContext: { orderId: object.externalId } }} />
```

### The Cards SDK

Load the SDK in the page you host:

```html
<script src="https://cdn.kustomerapp.com/card-js/latest/kustomer-card.min.js"></script>
```

Kustomer loads your page with `parentUrl` and `id` query parameters, and the SDK uses them to exchange `postMessage` events only with the Kustomer window that opened it. Your page must be served over HTTPS and allow framing by Kustomer.

```js title="card.js"
Kustomer.initialize(async (context, { userRoles }) => {
  render(context)
  Kustomer.resize()
})

Kustomer.on('context', (context) => {
  // The agent moved to another conversation or timeline item.
  render(context)
  Kustomer.resize()
})

async function refund(orderId) {
  try {
    await Kustomer.command.run('acme_orders.app.refund-order', { args: { orderId } })
    Kustomer.handleTriggerToast({ type: 'success', message: `Refunded order ${orderId}` })
  } catch ([err]) {
    Kustomer.updateStatus({ status: 'danger', description: 'Acme is unreachable. Refunds are paused.' })
  }
}
```

`context.type` is `customer`, `conversation`, `company`, `kobject.<klass>`, `widget`, `app` or your custom type. It carries the full JSON:API resources: `context.customer`, plus `context.conversation` or the KObject when one is selected.

| Method                                                                                                               | Does                                                                                                                          |
| -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `Kustomer.initialize(options?, callback)`                                                                            | Handshake. Calls back once with `(context, { userRoles })`; also returns a promise. `options.debug: true` logs every message. |
| `Kustomer.on(event, callback)`                                                                                       | Subscribe to `context`, `theme`, `flags`, `number-dialed`, `routing-status-change` or `kustomer-typing-status-changed`.       |
| `Kustomer.request({ url, method, body }, callback?)`                                                                 | Call the Kustomer API as the agent. Rejects with `[error, data]`.                                                             |
| `Kustomer.command.run(name, { args, headers, body })`                                                                | Run one of your app's commands. Resolves with the response's attributes.                                                      |
| `Kustomer.resize({ height?, width? })`                                                                               | Fit the iframe. With no height, measures your page.                                                                           |
| `Kustomer.updateStatus({ status, description })` / `Kustomer.clearStatus()`                                          | Show or clear a banner on the card: `info`, `warning` or `danger`.                                                            |
| `Kustomer.handleTriggerToast({ type, message })`                                                                     | Show a toast: `info`, `warning`, `error` or `success`.                                                                        |
| `Kustomer.openCustomer(id)` / `Kustomer.openCustomerEvent(customerId, eventId)`                                      | Navigate the agent.                                                                                                           |
| `Kustomer.expandInsightsCard({ appName, appLogo, title, height, url, context })` / `Kustomer.contractInsightsCard()` | Open a full-height view of the insights panel that stays open while the agent moves around the same timeline.                 |
| `Kustomer.open()` / `Kustomer.close()`                                                                               | Open a widget, or remove the iframe.                                                                                          |

### Modals

A card can open up to three modals, each its own page that also loads the SDK:

```js
await Kustomer.modal.init({ id: 'refund', url: 'https://kustomer.acme.example/refund', title: 'Refund order', additionalData: { orderId: '4417' } })
Kustomer.modal.show('refund')
// Later, from the modal or the card:
Kustomer.modal.hide('refund', 'Refund issued')
Kustomer.modal.destroy('refund')
```

`init` registers a modal without showing it, and fails once three are registered; `destroy` or `destroyAll` frees the slots. `show` hides any other open modal. `hide` can show a success toast. `resize({ height, width })` resizes the visible modal.

## Troubleshooting

| Symptom                               | Check                                                                                                                                                                                                       |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The card is blank                     | A JavaScript error in the template. Check the browser console, and make sure the template ends with exactly one JSX expression.                                                                             |
| `Cannot read properties of undefined` | Read optional data with `_.get`, and guard on `conversation` in customer-level views.                                                                                                                       |
| Text is invisible in dark mode        | Replace fixed colors with the [theme variables](#support-dark-mode), and the older `--font-*-color` names with `--text-*`.                                                                                  |
| The iframe never initializes          | It must load the Cards SDK and be framed by Kustomer (with `parentUrl` and `id` in its URL). Turn on `Kustomer.initialize({ debug: true }, …)`.                                                             |
| A command returns `400`               | "cannot run a command with unpermitted args": an `args` key isn't in `permittedArgs` (or a `urlArgs` key isn't in `permittedUrlArgs`). Otherwise the body breaks `inputSchema`, or the command is disabled. |
