App definition anatomy
Every top-level key of a Kustomer app definition, with types, validation rules, naming conventions and a complete example.
An app definition is one JSON object. Three keys are required (app, version and title); everything else is optional and adds a kind of component. The schema is strict: a top-level key Kustomer doesn't recognize fails registration rather than being ignored.
kviewsKViews
JSX templates rendered inside Kustomer: insight cards, detail rows, and the expanded timeline view of your Klass. Embed your own hosted UI with DynamicCard and the Cards SDK.
Guide: KViewsA complete example
This app syncs orders from a fictional store, Acme. It stores orders as a custom object, shows recent orders in the insights panel, receives order updates on an inbound hook, and lets workflows issue refunds through Acme's API with each org's own key.
{
"app": "acme_orders",
"version": "1.4.0",
"title": "Acme Orders",
"identifier": "acmeord",
"visibility": "private",
"description": "See Acme orders on every customer and refund them from a workflow.\n\nSends the order ID and refund amount to Acme when a workflow issues a refund.",
"iconUrl": "https://acme.example/kustomer/icon.png",
"screenshots": ["https://acme.example/kustomer/screenshot-1.jpg"],
"appDetails": {
"appDeveloper": { "name": "Acme", "website": "https://acme.example", "supportEmail": "support@acme.example" },
"externalPlatform": { "name": "Acme Commerce", "website": "https://acme.example/commerce" },
"documentationLinks": [{ "title": "Set up Acme for Kustomer", "url": "https://acme.example/docs/kustomer" }]
},
"tags": ["ecommerce"],
"dependencies": ["kustomer-^1.9.0"],
"settings": {
"default": {
"apiKey": { "type": "secret", "defaultValue": "", "required": true },
"storeDomain": { "type": "string", "defaultValue": "", "required": true }
}
},
"klasses": [
{
"name": "acme_orders-order",
"icon": "cart",
"color": "#5B6CF9",
"metadata": {
"properties": {
"orderNumberStr": { "displayName": "Order number" },
"statusStr": { "displayName": "Status" },
"totalNum": { "displayName": "Total" },
"trackingUrl": { "displayName": "Tracking" },
"placedAt": { "displayName": "Placed" }
}
}
}
],
"kviews": [
{
"name": "kustomer.app.acme_orders.order-card",
"resource": "kobject.acme_orders-order",
"context": "smartbar-card",
"meta": { "displayName": "Acme order", "icon": "cart" },
"template": "<Segment><BasicRow label=\"Status\" value={object.custom.statusStr} /><BasicRow label=\"Total\" value={object.custom.totalNum} /></Segment>"
}
],
"hooks": [
{
"eventName": "kustomer.app.acme_orders.order-updated",
"displayEventName": "Acme order updated",
"description": "Acme posts here whenever an order changes.",
"type": "web"
}
],
"triggers": [
{
"eventName": "kustomer.app.acme_orders.order-updated",
"description": "An order changed in Acme",
"outputTemplate": {
"orderId": "/#data.attributes.data.id",
"email": "/#data.attributes.data.customer.email",
"status": "/#data.attributes.data.status"
},
"outputSchema": {
"type": "object",
"properties": { "orderId": { "type": "string" }, "email": { "type": "string" }, "status": { "type": "string" } },
"additionalProperties": false
},
"sampleOutputContext": { "orderId": "4417", "email": "ada@example.com", "status": "shipped" }
}
],
"actions": [
{
"name": "kustomer.app.acme_orders.refund",
"description": "Refund an Acme order",
"type": "rest_api",
"appSettings": { "apiKey": { "key": "acme_orders.default.apiKey" } },
"inputTemplate": {
"uri": "https://api.acme.example/orders/{{orderId}}/refunds",
"method": "POST",
"headers": { "Authorization": "Bearer {{apiKey}}", "Content-Type": "application/json" },
"data": { "amount": "/#amount" },
"json": true
},
"inputSchema": {
"type": "object",
"properties": { "orderId": { "type": "string" }, "amount": { "type": "number" } },
"required": ["orderId"],
"additionalProperties": false
},
"outputTemplate": { "response": "/#response", "body": "/#body" },
"outputSchema": {
"type": "object",
"properties": { "response": { "type": "object" }, "body": { "type": "object" } },
"additionalProperties": false
},
"sampleOutputContext": { "body": { "id": "rf_19", "status": "pending" } }
}
],
"i18n": {
"en_us": {
"acme_orders.settings.page.title": "Acme Orders",
"acme_orders.settings.page.description": "Connect your Acme store.",
"acme_orders.settings.path.default.apiKey.displayName": "API key",
"acme_orders.settings.path.default.apiKey.description": "Create one in Acme under **Settings › API**.",
"acme_orders.settings.path.default.storeDomain.displayName": "Store domain",
"app.acme_orders.workflow.builder.actions.kustomer.app.acme_orders.refund.displayName": "Refund order",
"app.acme_orders.workflow.builder.actions.kustomer.app.acme_orders.refund.description": "Issue a full or partial refund in Acme.",
"app.acme_orders.workflow.builder.actions.kustomer.app.acme_orders.refund.properties.orderId.displayName": "Order ID",
"app.acme_orders.workflow.builder.actions.kustomer.app.acme_orders.refund.properties.amount.displayName": "Amount"
}
},
"changelog": {
"1.4.0": "{{October 2026}} Added the Refund order action.",
"1.3.0": "{{August 2026}} Orders show their tracking link."
}
}Identity and listing
appstringrequired- The app's unique name. Letters, digits and underscores only (
^[a-zA-Z0-9_]+$), up to 128 characters. No dashes or spaces. It can't change after the first registration, and Kustomer appends_<orgId>for private apps. versionstringrequired- A semantic version such as
1.4.0. Every registration must be greater than the latest registered version. Pre-release suffixes such as1.5.0-betaare allowed. titlestringrequired- The display name in the App Directory, settings menu and OAuth consent screen.
descriptionMarkdown- What the app does. For public apps, it must say what data the app sends to third parties, when, and why.
iconUrlURL- A PNG at least 1200×1200 px. Kustomer rounds the corners for you.
screenshotsURL[]- Images of the app in use, shown on the listing.
appDetailsobjectappDeveloper(name,website,supportEmail),externalPlatform(name,website) anddocumentationLinks([{ title, url }]). Replaces the deprecatedvendorandwebsitekeys, which registration now rejects.tagsstring[]- Directory categories:
ai,analytics,csat,ecommerce,messaging. instructionsMarkdown- Setup steps shown on the listing.
postInstallMessageMarkdown- Shown to the admin right after a successful install. Use it to say what to configure next.
releaseNotesUrlURL- Where you publish detailed release notes.
changelogobject- A map from version to a Markdown entry that starts with the release month in double braces:
"1.4.0": "{{October 2026}} Added refunds.". See Versioning.
identifierstring- A short, permanent ID used to namespace attributes the app adds to standard objects. Lowercase letters, digits and underscores, 2 to 8 characters. Kustomer generates one if you leave it out, and it can't change after the first registration. See Klasses.
visibilitystringprivate. Only Kustomer publishes public apps.dependenciesstring[]- Other apps this one needs, as
<app>-<semver range>, such as"kustomer-^1.9.0". Orgs must have a matching version installed. redirectUrisURL[]- Allowed OAuth redirect URIs when your service signs users in with Kustomer. See OAuth for apps.
metaobject- Free-form metadata stored with the version.
Components
Each of these keys installs something in the org. Component names follow the naming scheme kustomer.app.<app>.<name>, and Kustomer applies it for you if you don't.
cards is deprecated; use a KView with DynamicCard instead.
Shortcuts
A shortcut is a saved reply. Beyond inserting text, it can change the conversation when an agent uses it:
{
"shortcuts": [
{
"name": "acme-refund-issued",
"draft": { "text": "Your refund is on its way. It can take 5 to 10 business days to appear." },
"conversation": {
"tags": { "operator": "append", "value": ["refund"] },
"status": { "operator": "replace", "value": "done" }
}
}
]
}conversation can set tags, assignedToUsers and assignedToTeams (append, replace or remove), name, status and subStatus (replace or remove), snooze and queue (replace), and custom attributes (replace). Limit who sees the shortcut with accessTeams and accessUsers.
Templates
templates.charts and templates.bizRules add starting points to the reporting and business-rules builders, which admins use to build their own charts and rules. Business-rule templates hold the same criteria (with and and or groups of <object>_<attribute> conditions) and actions as a rule an admin builds. See Criteria for the condition format.
Validation rules
Registration checks the whole definition before creating anything.
See Register and install for fixes.
Organize a large definition
Real apps have dozens of workflows and long JSX templates, which are painful to edit inside one JSON string. Keep each component in its own file and assemble the definition in a build step:
acme-orders/
├── app.json # app, title, description, appDetails…
├── changelog.json
├── klasses/order.json
├── kviews/order-card.jsx
├── actions/refund.json
├── workflows/sync.json
├── i18n/en_us.json
└── build.mjsimport { readFileSync, readdirSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
const json = (p) => JSON.parse(readFileSync(p, 'utf8'))
const all = (dir) => readdirSync(dir).filter((f) => f.endsWith('.json')).map((f) => json(join(dir, f)))
const pkg = json('package.json')
const definition = {
...json('app.json'),
version: pkg.version,
changelog: json('changelog.json'),
klasses: all('klasses'),
kviews: [
{
name: 'kustomer.app.acme_orders.order-card',
resource: 'kobject.acme_orders-order',
context: 'smartbar-card',
meta: { displayName: 'Acme order', icon: 'cart' },
template: readFileSync('kviews/order-card.jsx', 'utf8'),
},
],
actions: all('actions'),
workflows: all('workflows'),
i18n: { en_us: json('i18n/en_us.json') },
}
writeFileSync('dist/acme_orders.json', JSON.stringify(definition, null, 2))Taking version from package.json means npm version minor bumps the app and the build together.