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.
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
{
"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>"
}
]
}namestringrequired- Globally unique. Use
kustomer.app.<app>.<view>; Kustomer applies that scheme if you don't. resourcestringrequired- The record type the view belongs to:
customer,company,conversationorkobject.<klass>. One resource per view, so a card for customers and conversations is two KViews. contextstringrequired- Where it renders. See the table below.
templatestringrequired- The JSX source, as a string.
meta.displayNamestring- The card's title, and its name in the KView editor.
meta.iconstring- A Material Design Icons name for the card header.
meta.statestringopento show the card expanded by default.
Where KViews appear
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:
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.
What's in scope
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 previews each one and lists every prop.
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:
<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:
<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:
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:
<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 previews both themes.
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.
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.
{
"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
}
}
]
}namestringrequired- Globally unique. Commands use the scheme
<app>.app.<name>, which Kustomer applies if you don't. urlstringrequired- The target URL. Interpolate settings and run-time arguments with triple braces, such as
{{{orderId}}}. httpMethodstringrequiredget,post,put,patchordelete, in lowercase.permittedArgsstring[]- Names callers may pass in
args: 3 to 32 letters, digits,-or_, starting with a letter.argsfill in the URL (string, number and boolean values only), headers and body. A run with any other name fails with400; a missing one renders as an empty string. permittedUrlArgsstring[]- Names callers may pass in
urlArgs, which fill in the URL only. appSettingsobject- Up to 20 settings to expose as template variables in the URL, headers and body:
{ "<variable>": { "key": "<app>.<category>.<name>" } }. Variable names follow the same rules aspermittedArgs. Use this for API keys. defaultHeadersobject- Headers sent on every run, rendered like the URL. Put the
Authorizationheader here so callers don't have to send it. Headers in the run request replace these by name. defaultBodyobject- The body to send when the run request has none. A run request's
body, even{}, replaces it entirely. appAccountNamestringdefaultto authenticate with the app's OAuth connection. See OAuth for apps.inputSchemaJSON Schema- Validates and casts the request body. Requests that miss
requiredproperties, or include propertiesadditionalProperties: falseforbids, are rejected. outputSchemaJSON Schema- Casts the response, and drops properties
additionalProperties: falsedoesn't allow. cacheSecondsnumber- 15 to 600. Enables response caching and cache fallback mode.
The app definition is rejected if a command has any other field.
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:
/v1/commands/{name}/run{
"args": { "storeDomain": "shop.acme.example", "orderId": "4417" }
}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=and&becomes&.{{{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,"") becomesnull. - Args win over settings. An arg with the same name as a setting replaces it.
inputSchemacasts the body before templates are filled in. A property typednumberthat holds"/#amount"becomesNaN, sent asnull. 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-idandx-kustomer-user-idheaders 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 inAuthorization: Bearer …, or in the auth config'scustomHeader.
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).
The agent’s browser sends this as is. Settings are filled in on Kustomer’s servers.
permittedArgs, inputSchema and the templates. Try passing an arg that isn’t in permittedArgs.Caching and cache fallback
With cacheSeconds set, Kustomer stores every response for 24 hours:
- A cached response younger than
cacheSecondsis returned without calling your API. - If your API returns a
4xxor5xx, 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.
<DynamicCard src={`https://kustomer.acme.example/card?customer=${customer.id}`} context={context} />Pass a custom context object to send your own data instead of the default:
<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:
<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.
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.
Modals
A card can open up to three modals, each its own page that also loads the SDK:
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.