@kerabie/mcp 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -22
- package/dist/{chunk-4MMO737J.js → chunk-LPASUBYU.js} +1 -1
- package/dist/chunk-PWWOTGWD.js +1054 -0
- package/dist/cli-http.js +2 -2
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/package.json +47 -47
- package/dist/chunk-72BI6YMN.js +0 -970
package/dist/chunk-72BI6YMN.js
DELETED
|
@@ -1,970 +0,0 @@
|
|
|
1
|
-
// src/tools.ts
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
var page = { page: z.number().int().min(1).optional().describe("Page number, from 1"), limit: z.number().int().min(1).max(100).optional().describe("Results per page (default 20, at most 100)") };
|
|
4
|
-
var id = (what) => ({ id: z.number().int().positive().describe(`The ${what} id`) });
|
|
5
|
-
var days = { days: z.number().int().min(1).max(365).optional().describe("Window in days (default 30)") };
|
|
6
|
-
var STATUS = z.enum(["WAITING", "ACTIVE", "RESOLVED", "CLOSED"]);
|
|
7
|
-
var PRIORITY = z.enum(["LOW", "NORMAL", "HIGH", "URGENT"]);
|
|
8
|
-
var CONTACT_TYPE = z.enum(["LEAD", "CUSTOMER", "VIP"]);
|
|
9
|
-
var contactFields = {
|
|
10
|
-
name: z.string().optional(),
|
|
11
|
-
email: z.string().email().optional(),
|
|
12
|
-
phone: z.string().optional(),
|
|
13
|
-
company: z.string().optional(),
|
|
14
|
-
notes: z.string().optional(),
|
|
15
|
-
contactType: CONTACT_TYPE.optional()
|
|
16
|
-
};
|
|
17
|
-
var articleFields = {
|
|
18
|
-
title: z.string().min(1),
|
|
19
|
-
content: z.string().min(1),
|
|
20
|
-
category: z.string().optional(),
|
|
21
|
-
type: z.enum(["faq", "article", "news", "information"]).optional(),
|
|
22
|
-
status: z.enum(["draft", "published", "archived"]).optional()
|
|
23
|
-
};
|
|
24
|
-
var campaignFields = {
|
|
25
|
-
name: z.string().min(1).max(80),
|
|
26
|
-
message: z.string().min(1).max(300).describe("What the visitor sees"),
|
|
27
|
-
ctaLabel: z.string().max(40).optional().describe("Button text; no button when empty"),
|
|
28
|
-
ctaAction: z.enum(["open_chat", "open_kb", "open_link"]).optional(),
|
|
29
|
-
ctaUrl: z.string().url().optional().describe("Only for open_link"),
|
|
30
|
-
trigger: z.enum(["page_load", "time_on_page", "scroll_depth", "exit_intent"]).optional(),
|
|
31
|
-
triggerValue: z.union([z.string(), z.number()]).optional().describe("Seconds for time_on_page, percent for scroll_depth"),
|
|
32
|
-
pages: z.string().optional().describe("Comma-separated URL patterns with * wildcards; empty = every page"),
|
|
33
|
-
audience: z.enum(["all", "new", "returning", "not_chatted", "country"]).optional(),
|
|
34
|
-
audienceCountry: z.string().length(2).optional().describe("ISO country code, required when audience is country"),
|
|
35
|
-
frequency: z.enum(["once_per_visitor", "once_per_day", "every_session"]).optional()
|
|
36
|
-
};
|
|
37
|
-
var toolParam = z.object({
|
|
38
|
-
name: z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/),
|
|
39
|
-
type: z.enum(["string", "number", "boolean"]),
|
|
40
|
-
description: z.string().max(300).optional(),
|
|
41
|
-
required: z.boolean().optional()
|
|
42
|
-
});
|
|
43
|
-
var aiToolFields = {
|
|
44
|
-
name: z.string().regex(/^[a-z][a-z0-9_]*$/).describe("snake_case function name the AI calls"),
|
|
45
|
-
displayName: z.string().min(2).max(80),
|
|
46
|
-
description: z.string().min(10).max(500).describe('Say when the AI should use the tool ("Use this when a customer asks\u2026")'),
|
|
47
|
-
endpoint: z.string().url().describe("A public https address; private and local addresses are rejected"),
|
|
48
|
-
method: z.enum(["GET", "POST", "PUT", "PATCH"]).optional(),
|
|
49
|
-
parameters: z.array(toolParam).max(20).optional(),
|
|
50
|
-
authType: z.enum(["none", "bearer", "api_key", "basic"]).optional(),
|
|
51
|
-
authConfig: z.record(z.string()).optional().describe("bearer: {token} \xB7 api_key: {header, value} \xB7 basic: {username, password}. Write-only."),
|
|
52
|
-
responsePath: z.string().optional().describe("Dot-notation path into the JSON reply, e.g. data.message"),
|
|
53
|
-
enabled: z.boolean().optional()
|
|
54
|
-
};
|
|
55
|
-
var TOOLS = [
|
|
56
|
-
// ── Conversations ─────────────────────────────────────────────────────────
|
|
57
|
-
{
|
|
58
|
-
name: "list_conversations",
|
|
59
|
-
title: "List conversations",
|
|
60
|
-
description: "List conversations in the inbox, newest first. Filter by status, channel, assigned agent or text.",
|
|
61
|
-
input: { status: STATUS.optional(), channel: z.enum(["WEBSITE", "WHATSAPP", "INSTAGRAM", "EMAIL", "API"]).optional(), search: z.string().optional(), assignedTo: z.number().int().optional(), ...page },
|
|
62
|
-
run: (c, a) => c.conversations.list(a)
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
name: "get_conversation",
|
|
66
|
-
title: "Get a conversation",
|
|
67
|
-
description: "One conversation with its customer, assigned agent, tags and whether the AI is handling it.",
|
|
68
|
-
input: id("conversation"),
|
|
69
|
-
run: (c, a) => c.conversations.get(a.id)
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: "get_customer_history",
|
|
73
|
-
title: "Get a customer's previous conversations",
|
|
74
|
-
description: "The same customer's other conversations, newest first.",
|
|
75
|
-
input: { ...id("conversation"), limit: z.number().int().min(1).max(20).optional() },
|
|
76
|
-
run: (c, a) => c.conversations.getHistory(a.id, a.limit)
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
name: "take_over_conversation",
|
|
80
|
-
title: "Take over from the AI",
|
|
81
|
-
description: "Stop the AI bot on a conversation and assign it to a team member if nobody holds it yet.",
|
|
82
|
-
write: true,
|
|
83
|
-
input: { ...id("conversation"), agentId: z.number().int().positive().describe("Organisation team-member id") },
|
|
84
|
-
run: (c, a) => c.conversations.takeOver(a.id, { agentId: a.agentId })
|
|
85
|
-
},
|
|
86
|
-
// ── Tickets ───────────────────────────────────────────────────────────────
|
|
87
|
-
{ name: "list_tickets", title: "List tickets", description: "List support tickets, newest first.", input: { status: STATUS.optional(), priority: PRIORITY.optional(), ...page }, run: (c, a) => c.tickets.list(a) },
|
|
88
|
-
{ name: "get_ticket_stats", title: "Ticket overview", description: "Open, unassigned and SLA-breaching ticket counts and average resolution time.", input: {}, run: (c) => c.tickets.getStats() },
|
|
89
|
-
{ name: "get_ticket", title: "Get a ticket", description: "One ticket with its messages.", input: id("ticket"), run: (c, a) => c.tickets.get(a.id) },
|
|
90
|
-
{
|
|
91
|
-
name: "create_ticket",
|
|
92
|
-
title: "Create a ticket",
|
|
93
|
-
description: "Create a ticket for an end user. Repeated calls with the same externalRef land on the same ticket.",
|
|
94
|
-
write: true,
|
|
95
|
-
input: { externalRef: z.string().min(1).describe("A stable identifier for the end user"), message: z.string().min(1), subject: z.string().optional(), priority: PRIORITY.optional(), name: z.string().optional(), email: z.string().email().optional() },
|
|
96
|
-
run: (c, a) => c.tickets.create(a)
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
name: "update_ticket",
|
|
100
|
-
title: "Update a ticket",
|
|
101
|
-
description: "Change a ticket's status or priority.",
|
|
102
|
-
write: true,
|
|
103
|
-
input: { ...id("ticket"), status: STATUS.optional(), priority: PRIORITY.optional() },
|
|
104
|
-
run: (c, a) => c.tickets.update(a.id, { status: a.status, priority: a.priority })
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
name: "reply_to_ticket",
|
|
108
|
-
title: "Reply to a ticket",
|
|
109
|
-
description: "Send a reply on a ticket. If the requester has an email, they receive it.",
|
|
110
|
-
write: true,
|
|
111
|
-
input: { ...id("ticket"), message: z.string().min(1) },
|
|
112
|
-
run: (c, a) => c.tickets.reply(a.id, a.message)
|
|
113
|
-
},
|
|
114
|
-
// ── Contacts ──────────────────────────────────────────────────────────────
|
|
115
|
-
{
|
|
116
|
-
name: "list_contacts",
|
|
117
|
-
title: "List contacts",
|
|
118
|
-
description: "Search contacts by name, email, phone, company or tag.",
|
|
119
|
-
input: { search: z.string().optional(), type: CONTACT_TYPE.optional(), sort: z.enum(["recent", "newest", "name"]).optional(), ...page },
|
|
120
|
-
run: (c, a) => c.contacts.list(a)
|
|
121
|
-
},
|
|
122
|
-
{ name: "get_contact_stats", title: "Contacts overview", description: "Totals by type and new contacts this week.", input: {}, run: (c) => c.contacts.getStats() },
|
|
123
|
-
{ name: "get_contact", title: "Get a contact", description: "One contact with their type, company, owner, notes, tags and last contact time.", input: id("contact"), run: (c, a) => c.contacts.get(a.id) },
|
|
124
|
-
{ name: "create_contact", title: "Create a contact", description: "Add a contact. Give at least a name or an email.", write: true, input: contactFields, run: (c, a) => c.contacts.create(a) },
|
|
125
|
-
{
|
|
126
|
-
name: "update_contact",
|
|
127
|
-
title: "Update a contact",
|
|
128
|
-
description: "Change fields on a contact. Send only what changes.",
|
|
129
|
-
write: true,
|
|
130
|
-
input: { ...id("contact"), ...contactFields },
|
|
131
|
-
run: (c, a) => {
|
|
132
|
-
const { id: contactId, ...rest } = a;
|
|
133
|
-
return c.contacts.update(contactId, rest);
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
// ── Knowledge base ────────────────────────────────────────────────────────
|
|
137
|
-
{
|
|
138
|
-
name: "search_knowledge_base",
|
|
139
|
-
title: "Search the Knowledge Base",
|
|
140
|
-
description: "Find articles by text. These are the articles the AI chatbot answers from.",
|
|
141
|
-
input: { query: z.string().min(1), ...page },
|
|
142
|
-
run: (c, a) => c.kb.list({ search: a.query, page: a.page, limit: a.limit })
|
|
143
|
-
},
|
|
144
|
-
{ name: "get_kb_stats", title: "Knowledge Base overview", description: "Article totals (published, drafts), total views, the most viewed article and the categories in use.", input: {}, run: (c) => c.kb.getStats() },
|
|
145
|
-
{ name: "get_kb_insights", title: "What to write or fix next", description: "Most viewed articles, customer questions the AI could not answer in the last 30 days (content gaps), lowest-rated articles and published articles not updated in 90 days.", input: {}, run: (c) => c.kb.getInsights() },
|
|
146
|
-
{ name: "get_article", title: "Get an article", description: "One Knowledge Base article.", input: id("article"), run: (c, a) => c.kb.get(a.id) },
|
|
147
|
-
{ name: "create_article", title: "Create an article", description: "Add a Knowledge Base article. Published articles are used by the AI chatbot straight away.", write: true, input: articleFields, run: (c, a) => c.kb.createArticle(a) },
|
|
148
|
-
{
|
|
149
|
-
name: "update_article",
|
|
150
|
-
title: "Update an article",
|
|
151
|
-
description: "Edit an article. Send only what changes.",
|
|
152
|
-
write: true,
|
|
153
|
-
input: { ...id("article"), title: articleFields.title.optional(), content: articleFields.content.optional(), category: articleFields.category, type: articleFields.type, status: articleFields.status },
|
|
154
|
-
run: (c, a) => {
|
|
155
|
-
const { id: articleId, ...rest } = a;
|
|
156
|
-
return c.kb.updateArticle(articleId, rest);
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
{ name: "delete_article", title: "Delete an article", description: "Permanently delete a Knowledge Base article.", write: true, destructive: true, input: id("article"), run: async (c, a) => {
|
|
160
|
-
await c.kb.deleteArticle(a.id);
|
|
161
|
-
return { success: true };
|
|
162
|
-
} },
|
|
163
|
-
// ── Campaigns ─────────────────────────────────────────────────────────────
|
|
164
|
-
{
|
|
165
|
-
name: "list_campaigns",
|
|
166
|
-
title: "List campaigns",
|
|
167
|
-
description: "Proactive messages shown on the chat widget, with how many times each was shown, opened, clicked and replied to.",
|
|
168
|
-
input: { status: z.enum(["active", "paused", "draft"]).optional(), search: z.string().optional(), ...page },
|
|
169
|
-
run: (c, a) => c.campaigns.list(a)
|
|
170
|
-
},
|
|
171
|
-
{ name: "get_campaign_stats", title: "Campaigns overview", description: "Totals, funnel numbers and how many campaigns the plan lets you run at once (limit, 0 = unlimited).", input: {}, run: (c) => c.campaigns.getStats() },
|
|
172
|
-
{ name: "get_campaign", title: "Get a campaign", description: "One campaign with its message, rules (trigger, pages, audience, frequency) and results.", input: id("campaign"), run: (c, a) => c.campaigns.get(a.id) },
|
|
173
|
-
{
|
|
174
|
-
name: "create_campaign",
|
|
175
|
-
title: "Create a campaign",
|
|
176
|
-
description: "Save a campaign as a draft, or launch it with isActive (counts against the plan's live-campaign limit).",
|
|
177
|
-
write: true,
|
|
178
|
-
input: { ...campaignFields, isActive: z.boolean().optional() },
|
|
179
|
-
run: (c, a) => c.campaigns.create(a)
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
name: "update_campaign",
|
|
183
|
-
title: "Update a campaign",
|
|
184
|
-
description: "Edit a campaign and/or turn it on or off with isActive. Send only what changes.",
|
|
185
|
-
write: true,
|
|
186
|
-
input: { ...id("campaign"), ...Object.fromEntries(Object.entries(campaignFields).map(([k, v]) => [k, v.optional()])), isActive: z.boolean().optional() },
|
|
187
|
-
run: (c, a) => {
|
|
188
|
-
const { id: campaignId, ...rest } = a;
|
|
189
|
-
return c.campaigns.update(campaignId, rest);
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
{ name: "duplicate_campaign", title: "Duplicate a campaign", description: "A copy saved as a draft with fresh results.", write: true, input: id("campaign"), run: (c, a) => c.campaigns.duplicate(a.id) },
|
|
193
|
-
{ name: "delete_campaign", title: "Delete a campaign", description: "Permanently delete a campaign and its results.", write: true, destructive: true, input: id("campaign"), run: (c, a) => c.campaigns.delete(a.id) },
|
|
194
|
-
// ── Analytics ─────────────────────────────────────────────────────────────
|
|
195
|
-
{ name: "get_analytics_summary", title: "Analytics summary", description: "Conversations, resolution rate, messages, AI chats, satisfaction and changes against the previous period.", input: days, run: (c, a) => c.analytics.getSummary(a) },
|
|
196
|
-
{ name: "get_analytics_insights", title: "Analytics insights", description: "Response times, busiest times, top questions, questions the AI could not answer, channels, countries, new vs returning customers and pages that start conversations.", input: days, run: (c, a) => c.analytics.getInsights(a) },
|
|
197
|
-
{ name: "get_agent_stats", title: "Agent performance", description: "Per-agent conversations, resolution rate, response time and rating.", input: days, run: (c, a) => c.analytics.getAgentStats(a) },
|
|
198
|
-
{ name: "get_ticket_sla", title: "Ticket SLA", description: "Tickets within SLA and the oldest waiting ones by priority.", input: {}, run: (c) => c.analytics.getTicketSla() },
|
|
199
|
-
{ name: "get_setup_status", title: "Setup status", description: "Whether a channel, the AI and an automation are set up.", input: {}, run: (c) => c.analytics.getSetupStatus() },
|
|
200
|
-
// ── Visitors and store ────────────────────────────────────────────────────
|
|
201
|
-
{
|
|
202
|
-
name: "list_visitors",
|
|
203
|
-
title: "List visitors",
|
|
204
|
-
description: "Recent and live website visitors.",
|
|
205
|
-
input: { status: z.enum(["all", "online", "offline", "blocked", "allowed"]).optional(), search: z.string().optional(), country: z.string().length(2).optional(), days: z.number().int().min(1).max(30).optional(), ...page },
|
|
206
|
-
run: (c, a) => c.visitors.list(a)
|
|
207
|
-
},
|
|
208
|
-
{ name: "get_visitors_overview", title: "Visitors overview", description: "Online now, countries, returning visitors, blocked IPs and top countries.", input: {}, run: (c) => c.visitors.getOverview() },
|
|
209
|
-
{ name: "list_store_products", title: "List store products", description: "Products in the Kerabie store.", input: page, run: (c, a) => c.store.list(a) },
|
|
210
|
-
// ── AI tools ──────────────────────────────────────────────────────────────
|
|
211
|
-
{ name: "list_ai_tools", title: "List AI tools", description: "Custom API actions the chatbot can call, with 30-day usage and the plan's tool limit (-1 = not on the plan, 0 = unlimited).", input: {}, run: (c) => c.aiTools.list() },
|
|
212
|
-
{ name: "get_ai_tool", title: "Get an AI tool", description: "One AI tool. Secrets are never returned.", input: id("tool"), run: (c, a) => c.aiTools.get(a.id) },
|
|
213
|
-
{ name: "create_ai_tool", title: "Create an AI tool", description: "Add an API the chatbot can call. Endpoints must be public addresses; the plan limits how many tools you can have.", write: true, input: aiToolFields, run: (c, a) => c.aiTools.create(a) },
|
|
214
|
-
{
|
|
215
|
-
name: "update_ai_tool",
|
|
216
|
-
title: "Update an AI tool",
|
|
217
|
-
description: "Edit a tool and/or switch it on or off with enabled. Send only what changes; leave a secret out to keep the stored one.",
|
|
218
|
-
write: true,
|
|
219
|
-
input: { ...id("tool"), ...Object.fromEntries(Object.entries(aiToolFields).map(([k, v]) => [k, v.optional()])) },
|
|
220
|
-
run: (c, a) => {
|
|
221
|
-
const { id: toolId, ...rest } = a;
|
|
222
|
-
return c.aiTools.update(toolId, rest);
|
|
223
|
-
}
|
|
224
|
-
},
|
|
225
|
-
{ name: "duplicate_ai_tool", title: "Duplicate an AI tool", description: "A copy that starts switched off.", write: true, input: id("tool"), run: (c, a) => c.aiTools.duplicate(a.id) },
|
|
226
|
-
{ name: "delete_ai_tool", title: "Delete an AI tool", description: "Permanently delete an AI tool.", write: true, destructive: true, input: id("tool"), run: (c, a) => c.aiTools.delete(a.id) },
|
|
227
|
-
{
|
|
228
|
-
name: "test_ai_tool",
|
|
229
|
-
title: "Test an AI tool",
|
|
230
|
-
description: "Calls the tool's real endpoint once with the given arguments and returns what the AI would see. Limited to 40 a hour.",
|
|
231
|
-
write: true,
|
|
232
|
-
input: { ...id("tool"), args: z.record(z.unknown()).optional().describe("Values for the tool's parameters") },
|
|
233
|
-
run: (c, a) => c.aiTools.test(a.id, a.args ?? {})
|
|
234
|
-
},
|
|
235
|
-
// ── MCP servers as tool sources ───────────────────────────────────────────
|
|
236
|
-
{ name: "list_mcp_servers", title: "List connected MCP servers", description: "Remote MCP servers whose tools the AI chatbot can call: the tools each offers, which are switched on, and call counts.", input: {}, run: (c) => c.mcpServers.list() },
|
|
237
|
-
{
|
|
238
|
-
name: "connect_mcp_server",
|
|
239
|
-
title: "Connect an MCP server",
|
|
240
|
-
description: "Connect a remote MCP server (public Streamable HTTP address) to the chatbot. No tool is switched on until you enable it with update_mcp_server.",
|
|
241
|
-
write: true,
|
|
242
|
-
input: { name: z.string().min(2).max(60), description: z.string().min(10).max(300).describe("What this server is for and when the chatbot should use it. The chatbot reads it to choose between servers."), url: z.string().url(), authType: z.enum(["none", "bearer", "api_key"]).optional(), authConfig: z.record(z.string()).optional().describe("bearer: {token} \xB7 api_key: {header, value}. Write-only.") },
|
|
243
|
-
run: (c, a) => c.mcpServers.create(a)
|
|
244
|
-
},
|
|
245
|
-
{ name: "sync_mcp_server", title: "Sync an MCP server", description: "Read the server\u2019s tools again. Switched-on tools it no longer offers are dropped.", write: true, input: id("MCP server"), run: (c, a) => c.mcpServers.sync(a.id) },
|
|
246
|
-
{
|
|
247
|
-
name: "update_mcp_server",
|
|
248
|
-
title: "Update an MCP server",
|
|
249
|
-
description: "Rename it, switch the whole server on or off, or set which of its tools (enabledTools) the chatbot may use. Switched-on tools count against the plan\u2019s AI tool limit.",
|
|
250
|
-
write: true,
|
|
251
|
-
input: { ...id("MCP server"), name: z.string().min(2).max(60).optional(), description: z.string().min(10).max(300).optional(), enabled: z.boolean().optional(), enabledTools: z.array(z.string()).max(100).optional(), authType: z.enum(["none", "bearer", "api_key"]).optional(), authConfig: z.record(z.string()).optional() },
|
|
252
|
-
run: (c, a) => {
|
|
253
|
-
const { id: serverId, ...rest } = a;
|
|
254
|
-
return c.mcpServers.update(serverId, rest);
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
name: "test_mcp_server_tool",
|
|
259
|
-
title: "Test an MCP server tool",
|
|
260
|
-
description: "Runs one of the connected server\u2019s tools once and returns what the AI would see. Limited to 40 a hour.",
|
|
261
|
-
write: true,
|
|
262
|
-
input: { ...id("MCP server"), tool: z.string().min(1).describe("The tool\u2019s name on that server"), args: z.record(z.unknown()).optional() },
|
|
263
|
-
run: (c, a) => c.mcpServers.test(a.id, a.tool, a.args ?? {})
|
|
264
|
-
},
|
|
265
|
-
{ name: "disconnect_mcp_server", title: "Disconnect an MCP server", description: "Remove a connected MCP server from the chatbot.", write: true, destructive: true, input: id("MCP server"), run: (c, a) => c.mcpServers.delete(a.id) }
|
|
266
|
-
];
|
|
267
|
-
|
|
268
|
-
// src/generated/knowledge.json
|
|
269
|
-
var knowledge_default = { builtAt: "2026-10-07T22:20:47.140Z", docs: [{ id: "agents/availability", url: "https://kerabie.com/docs/agents/availability", title: "Agent Availability", description: "Set your status and manage online presence.", section: "agents", body: "Agent availability controls whether you receive new conversations and appear as online to visitors.\n\n## Status options\n\n| Status | Meaning |\n|---|---|\n| **Available** | You can receive new conversations and are shown as online |\n| **Busy** | You are logged in but not taking new conversations |\n| **Away** | You are temporarily offline \u2014 conversations go to the queue |\n| **Offline** | Fully signed out |\n\n## Changing your status\n\nClick your **avatar** in the bottom-left of the sidebar, then choose a status from the dropdown. The change is instant.\n\nWhen you set yourself to **Busy** or **Away**, new conversations are not auto-assigned to you. They remain in the unassigned queue or are routed to other available agents.\n\n## Automatic away\n\nIf you close the browser tab or have no activity for a configurable period, Kerabie can automatically set you to Away.\n\nAdmins configure this timeout under **Settings \u2192 General \u2192 Auto-away after**.\n\n## Online indicator in the widget\n\nIf at least one agent in your workspace is **Available**, the widget shows a green indicator to visitors. When no agents are available, the indicator turns grey and the widget falls back to offline mode (collecting an email for async replies).\n\nConfigure offline mode messaging under **Settings \u2192 Chat Widget \u2192 Offline mode**.\n\n## Availability and round-robin\n\nWhen a conversation is assigned to a team using round-robin, only agents who are **Available** are eligible to receive it. Agents set to Busy or Away are skipped until they return to Available.\n\n## Agent schedule *(Pro)*\n\nOn Pro and above, admins can set working hours per agent under **Settings \u2192 Agents \u2192 [agent] \u2192 Schedule**. Outside those hours the agent is automatically set to Away even if they're logged in.", headings: [{ level: 2, text: "Status options" }, { level: 2, text: "Changing your status" }, { level: 2, text: "Automatic away" }, { level: 2, text: "Online indicator in the widget" }, { level: 2, text: "Availability and round-robin" }, { level: 2, text: "Agent schedule *(Pro)*" }] }, { id: "agents/inviting", url: "https://kerabie.com/docs/agents/inviting", title: "Inviting Agents", description: "Add team members to your Kerabie workspace.", section: "agents", body: "Any admin can invite new agents to the workspace. Invited agents get an email with a link to set their password and join.\n\n## Sending an invitation\n\n1. Go to **Settings \u2192 Agents \u2192 Invite agent**\n2. Enter the agent's **email address**\n3. Choose their **role** \u2014 Agent or Admin (see [Roles & Permissions](/docs/agents/roles))\n4. Optionally assign them to one or more **teams**\n5. Click **Send invite**\n\nThe invitation is valid for **7 days**. If it expires, send a new one from the same screen.\n\n## What the invited agent sees\n\nThe agent receives an email from `noreply@kerabie.com` with a **Join workspace** button. Clicking it opens a page where they set their name and password. After that they land directly in the dashboard.\n\n## Resending or cancelling an invitation\n\nIn **Settings \u2192 Agents**, pending invitations appear with a **Pending** badge. Click the kebab menu (\u22EF) next to a pending invite to:\n\n- **Resend** \u2014 sends the email again with a fresh 7-day link\n- **Cancel** \u2014 invalidates the link immediately\n\n## Agent limits by plan\n\n| Plan | Max agents |\n|---|---|\n| Free | 2 |\n| Starter | 5 |\n| Pro | 20 |\n| Business | Unlimited |\n\nIf you've reached your plan's limit, the invite button is disabled. Upgrade under **Settings \u2192 Billing** to add more seats.\n\n## Removing an agent\n\n1. Go to **Settings \u2192 Agents**\n2. Click the agent's name\n3. Click **Remove from workspace**\n\nRemoved agents lose access immediately. Their past conversations remain in the system and can be reassigned.", headings: [{ level: 2, text: "Sending an invitation" }, { level: 2, text: "What the invited agent sees" }, { level: 2, text: "Resending or cancelling an invitation" }, { level: 2, text: "Agent limits by plan" }, { level: 2, text: "Removing an agent" }] }, { id: "agents/roles", url: "https://kerabie.com/docs/agents/roles", title: "Roles & Permissions", description: "Control what agents can see and do.", section: "agents", body: "Kerabie has two built-in roles: **Agent** and **Admin**.\n\n## Role comparison\n\n| Permission | Agent | Admin |\n|---|---|---|\n| View and reply to assigned conversations | \u2705 | \u2705 |\n| View all conversations (unassigned + team) | \u2705 | \u2705 |\n| Use quick responses | \u2705 | \u2705 |\n| Add tags and notes | \u2705 | \u2705 |\n| Assign / reassign conversations | \u2705 | \u2705 |\n| Create and edit quick responses | \u274C | \u2705 |\n| Invite and remove agents | \u274C | \u2705 |\n| Create and manage teams | \u274C | \u2705 |\n| Configure automations | \u274C | \u2705 |\n| Manage knowledge base | \u274C | \u2705 |\n| Access billing and subscription | \u274C | \u2705 |\n| Generate and revoke API keys | \u274C | \u2705 |\n| View analytics | \u274C | \u2705 |\n| Change widget settings | \u274C | \u2705 |\n\n## Changing a role\n\n1. Go to **Settings \u2192 Agents**\n2. Click the agent's name\n3. Change the **Role** dropdown to Agent or Admin\n4. Save\n\nThe change takes effect on the agent's next page load.\n\n## Owner\n\nThe workspace **owner** is the user who created the organisation. The owner cannot be demoted to Agent and cannot be removed by other admins. Only the owner can delete the workspace entirely.\n\nTo transfer ownership, contact support.\n\n## Multiple admins\n\nYou can have as many admins as needed \u2014 there is no limit on admin seats. It's good practice to have at least two admins so the workspace is never locked if one person leaves.", headings: [{ level: 2, text: "Role comparison" }, { level: 2, text: "Changing a role" }, { level: 2, text: "Owner" }, { level: 2, text: "Multiple admins" }] }, { id: "agents/teams", url: "https://kerabie.com/docs/agents/teams", title: "Teams", description: "Group agents into teams for conversation routing.", section: "agents", body: "Teams let you group agents so conversations can be routed to the right group rather than a specific individual. This makes routing more resilient \u2014 if one agent is unavailable, another team member picks it up.\n\n## Creating a team\n\n1. Go to **Settings \u2192 Teams \u2192 New team**\n2. Enter a **team name** (e.g. Sales, Support, Billing)\n3. Add agents to the team\n4. Save\n\n## Assigning conversations to a team\n\n**Manually** \u2014 in the conversation panel, click **Assign \u2192 Team** and choose a team.\n\n**Automatically** \u2014 use an automation:\n- Trigger: `Conversation created`\n- Action: `Assign team \u2192 [team name]`\n\nSee [Automations](/docs/automations/overview) for the full setup.\n\n## Round-robin distribution\n\nWhen a conversation is assigned to a team, Kerabie uses round-robin to give it to the next **Available** agent on the team. The cycle resets when all agents have been assigned once.\n\nIf no team member is Available, the conversation stays in the team queue until someone comes online.\n\n## Team inbox\n\nAgents see conversations assigned to their teams in the **Team** section of the conversations inbox, alongside their personally assigned conversations.\n\n## Multiple teams\n\nAn agent can belong to multiple teams. They receive conversations from all teams they're on, in the same shared inbox.\n\n## Removing agents from a team\n\n1. Go to **Settings \u2192 Teams \u2192 [team name]**\n2. Click the **\xD7** next to an agent's name\n3. Save\n\nAny conversations currently assigned to that team remain assigned to the team \u2014 they are not unassigned from existing members.", headings: [{ level: 2, text: "Creating a team" }, { level: 2, text: "Assigning conversations to a team" }, { level: 2, text: "Round-robin distribution" }, { level: 2, text: "Team inbox" }, { level: 2, text: "Multiple teams" }, { level: 2, text: "Removing agents from a team" }] }, { id: "agents/virtual-assistants", url: "https://kerabie.com/docs/agents/virtual-assistants", title: "Virtual Assistants", description: "Hire verified support agents from the Kerabie marketplace.", section: "agents", body: "The Kerabie Marketplace connects you with verified virtual assistants (VAs) \u2014 trained support agents who handle conversations on your behalf.\n\n<Note>The VA Marketplace is available on **Pro** and **Business** plans.</Note>\n\n## What is a virtual assistant?\n\nA virtual assistant is a real human agent who joins your Kerabie workspace and handles conversations as if they were a member of your team. They are:\n\n- **Vetted** \u2014 background-checked and tested on support quality\n- **Trained on your docs** \u2014 they read your knowledge base and playbook before going live\n- **Managed through Kerabie** \u2014 conversations, notes, and handoffs all happen in your existing inbox\n\n## Hiring a VA\n\n1. Go to **Marketplace** in the sidebar (or [kerabie.com/app/marketplace](https://kerabie.com/app/marketplace))\n2. Browse available VAs \u2014 filter by language, availability, rating, and speciality\n3. Click a profile to read their bio, experience, and reviews\n4. Click **Hire** and select a plan (per-hour or monthly retainer)\n5. The VA is added as an agent to your workspace within 24 hours\n\n## Onboarding your VA\n\nAfter hiring, share your:\n- Knowledge base \u2014 the VA reads all published articles\n- Brand tone guide \u2014 set this under **Settings \u2192 Widget Settings \u2192 Bot personality**\n- Any special instructions \u2014 add them to the **VA Playbook** in Marketplace settings\n\n## Monitoring VA performance\n\nTrack your VA's activity in **Analytics \u2192 Agents**:\n- Response time\n- CSAT scores\n- Conversations handled per day\n- Resolution rate\n\n## Pausing or ending a contract\n\n1. Go to **Marketplace \u2192 My VAs**\n2. Click the VA's name\n3. Choose **Pause** (temporary) or **End contract** (permanent)\n\nThe VA's access to your workspace is removed within 1 hour of ending the contract. All conversation history is retained.\n\n## VA availability\n\nVAs set their own availability hours. Check a VA's timezone and hours on their profile page before hiring. If you need 24/7 coverage, hire multiple VAs in different time zones.\n\n## Job board\n\nPrefer to let VAs come to you? Post a job instead of searching.\n\n1. Go to **Marketplace \u2192 Job board** and choose **Post a job**.\n2. Add a title, a description, the pay type (hourly or fixed price), your budget, how long the work runs, and the skills you need.\n3. VAs send you proposals with their rate and a cover letter. Open **Marketplace \u2192 Activity \u2192 Posted jobs** to read them.\n4. **Accept** a proposal to send that VA a hire request at their proposed rate, or **Decline** it. **Close job** when you have found someone.\n\nHow many jobs you can have open at once depends on your plan. Closed jobs never count toward the limit.\n\n**Applying as a VA:** open the job board, choose **Apply now**, and send your hourly rate with a short cover letter. You need a VA profile first. Track your proposals under **Activity \u2192 Applications**, and withdraw one at any time before it is answered. You cannot apply to a job posted by your own organisation.\n\n## Job board\n\nInstead of searching for a VA yourself, post a job and let VAs come to you.\n\n1. Go to **Marketplace \u2192 Job board** and choose **Post a job**.\n2. Add a title, a description, the pay type (hourly or fixed price), your budget, how long the work lasts and the skills you need.\n3. VAs send you a proposal with the hourly rate they would charge and a short cover letter.\n4. Open **Marketplace \u2192 Activity \u2192 Posted jobs** to read the proposals. **Accept** sends that VA a normal hire request at their proposed rate. **Decline** closes the proposal.\n5. **Close job** takes the job off the board.\n\nHow many jobs you can keep open at once depends on your plan. Closed jobs never count towards it.\n\nIf you are a VA, browse the board, choose **Apply now**, and track what you sent under **Activity \u2192 Applications**. You can withdraw a proposal until it is answered. You need a VA profile to apply, and you cannot apply to a job posted by your own organisation.", headings: [{ level: 2, text: "What is a virtual assistant?" }, { level: 2, text: "Hiring a VA" }, { level: 2, text: "Onboarding your VA" }, { level: 2, text: "Monitoring VA performance" }, { level: 2, text: "Pausing or ending a contract" }, { level: 2, text: "VA availability" }, { level: 2, text: "Job board" }, { level: 2, text: "Job board" }] }, { id: "ai/escalation", url: "https://kerabie.com/docs/ai/escalation", title: "Escalation to Human Agent", description: "How the AI hands off conversations to your team.", section: "ai", body: `When the AI can't confidently answer a question, it escalates the conversation to a human agent rather than guessing.
|
|
270
|
-
|
|
271
|
-
## When escalation happens
|
|
272
|
-
|
|
273
|
-
- AI confidence falls below your threshold (default: 70%)
|
|
274
|
-
- Visitor explicitly asks for a human ("speak to a person", "talk to someone")
|
|
275
|
-
- Visitor is frustrated (negative sentiment detected)
|
|
276
|
-
- Conversation matches an escalation trigger you've set up in Automations
|
|
277
|
-
|
|
278
|
-
## What happens during escalation
|
|
279
|
-
|
|
280
|
-
1. The AI sends a handoff message: *"Let me connect you with a team member."*
|
|
281
|
-
2. The conversation is flagged as **needs agent**
|
|
282
|
-
3. Available agents are notified (push, email, or both \u2014 based on their preferences)
|
|
283
|
-
4. The conversation appears in the agents' queue with a summary of what was discussed
|
|
284
|
-
|
|
285
|
-
## Escalation message
|
|
286
|
-
|
|
287
|
-
Customise the message visitors see when the AI escalates:
|
|
288
|
-
|
|
289
|
-
In **Settings \u2192 AI \u2192 Escalation message**:
|
|
290
|
-
\`\`\`
|
|
291
|
-
I wasn't able to fully answer your question. I'm connecting you with a team member now \u2014 they'll be with you shortly.
|
|
292
|
-
\`\`\`
|
|
293
|
-
|
|
294
|
-
## Outside business hours
|
|
295
|
-
|
|
296
|
-
If no agents are online when escalation happens:
|
|
297
|
-
|
|
298
|
-
1. The AI sends your **offline message** ("Our team is offline, but we'll reply within a few hours")
|
|
299
|
-
2. The visitor can leave their email
|
|
300
|
-
3. The org owner receives an email notification immediately
|
|
301
|
-
|
|
302
|
-
Configure offline behavior in [Settings \u2192 Business Hours](/docs/widget/offline-mode).
|
|
303
|
-
|
|
304
|
-
## Escalation analytics
|
|
305
|
-
|
|
306
|
-
Track escalation rates in **Analytics \u2192 AI** to see:
|
|
307
|
-
|
|
308
|
-
- Which questions the AI struggles with most (add these to your knowledge base)
|
|
309
|
-
- Average time from escalation to first human response
|
|
310
|
-
- Escalation rate trend over time`, headings: [{ level: 2, text: "When escalation happens" }, { level: 2, text: "What happens during escalation" }, { level: 2, text: "Escalation message" }, { level: 2, text: "Outside business hours" }, { level: 2, text: "Escalation analytics" }] }, { id: "ai/flows", url: "https://kerabie.com/docs/ai/flows", title: "AI Message Flows", description: "Build decision-tree flows for guided, automated conversations.", section: "ai", body: 'AI Message Flows combine the structure of decision trees with AI-generated responses. Unlike pure automation flows that follow rigid scripts, AI flows can handle natural language input at each step while still following a defined path.\n\n<Note>AI Message Flows require the AI assistant to be enabled and are available on **Pro** and above.</Note>\n\n## Flow vs automation\n\n| | Automation flow | AI message flow |\n|---|---|---|\n| Visitor input | Multiple-choice buttons only | Free text understood by AI |\n| Branching | Fixed conditions | AI interprets intent |\n| Fallback | Assign to agent | AI tries to answer, then assigns |\n| Use case | Simple triage | Complex guided conversations |\n\n## Creating an AI flow\n\n1. Go to **Settings \u2192 AI Assistant \u2192 Flows \u2192 New flow**\n2. Give the flow a name\n3. Build steps on the canvas \u2014 each step can be:\n - **AI message** \u2014 the AI generates a response based on the step\'s topic prompt\n - **Question** \u2014 fixed multiple-choice options for the visitor\n - **Condition** \u2014 branch based on visitor attribute or prior answer\n - **Assign** \u2014 hand off to an agent or team\n - **End** \u2014 close the flow\n4. Set the **topic prompt** on each AI message step \u2014 this guides the AI to stay on topic\n5. Click **Activate**\n\n## Example: product support flow\n\n```\n[AI message] "Hi! What product are you asking about?"\n \u2193 (visitor types "billing" or "invoice" etc.)\n[Condition] Topic = Billing\n \u2192 [AI message] topic: "Answer billing and invoice questions" \u2192 [End]\n \u2192 [Assign] Support team \u2192 [End]\n```\n\n## Keeping flows on topic\n\nEach **AI message** step has a **topic prompt** \u2014 a short instruction that constrains what the AI talks about at that step. Without it, the AI answers anything, which may steer the conversation off the intended path.\n\nExample topic prompt: `"Only answer questions about subscription cancellation and refunds. If asked about anything else, say you\'ll connect them with a human."`\n\n## Fallback behaviour\n\nIf the AI cannot handle a visitor\'s message at any step, the flow automatically escalates to **Assign** using the team or agent configured in **Settings \u2192 AI Assistant \u2192 Default escalation**.', headings: [{ level: 2, text: "Flow vs automation" }, { level: 2, text: "Creating an AI flow" }, { level: 2, text: "Example: product support flow" }, { level: 2, text: "Keeping flows on topic" }, { level: 2, text: "Fallback behaviour" }] }, { id: "ai/knowledge-base", url: "https://kerabie.com/docs/ai/knowledge-base", title: "AI & Knowledge Base", description: "Connect your knowledge base to power accurate AI responses.", section: "ai", body: `The Kerabie AI draws all its answers from your published knowledge base articles. There is no separate AI training step \u2014 publish an article and it's immediately available to the AI.
|
|
311
|
-
|
|
312
|
-
## How the AI uses articles
|
|
313
|
-
|
|
314
|
-
When a visitor sends a message, the AI:
|
|
315
|
-
|
|
316
|
-
1. **Searches** your knowledge base for articles relevant to the question
|
|
317
|
-
2. **Reads** the matched articles in full
|
|
318
|
-
3. **Generates** a response grounded in that content
|
|
319
|
-
4. **Cites** the article title so the visitor can read the full page
|
|
320
|
-
|
|
321
|
-
If no relevant article is found, the AI says it doesn't know and offers to connect with a human agent.
|
|
322
|
-
|
|
323
|
-
## Writing articles the AI understands well
|
|
324
|
-
|
|
325
|
-
The AI works best with articles that are:
|
|
326
|
-
|
|
327
|
-
- **Specific** \u2014 one topic per article, not a wall of text covering everything
|
|
328
|
-
- **Direct** \u2014 answer the question in the first paragraph, then add detail
|
|
329
|
-
- **Keyword-rich** \u2014 include the exact phrases visitors use (e.g. "cancel subscription", "forgot password")
|
|
330
|
-
- **Up to date** \u2014 outdated answers are worse than no answer; archive articles that no longer apply
|
|
331
|
-
|
|
332
|
-
Avoid:
|
|
333
|
-
- Marketing language and filler ("We're excited to offer...")
|
|
334
|
-
- Ambiguous pronouns without clear referents
|
|
335
|
-
- Answers that depend on context the AI can't know (e.g. "call us at...")
|
|
336
|
-
|
|
337
|
-
## Controlling AI access per article
|
|
338
|
-
|
|
339
|
-
By default the AI can use all published articles. To exclude a specific article:
|
|
340
|
-
|
|
341
|
-
1. Open the article in the knowledge base editor
|
|
342
|
-
2. Toggle **Exclude from AI** in the sidebar
|
|
343
|
-
3. Save
|
|
344
|
-
|
|
345
|
-
The article still appears in the FAQ widget and search \u2014 it's only hidden from AI responses.
|
|
346
|
-
|
|
347
|
-
## Checking what the AI knows
|
|
348
|
-
|
|
349
|
-
Under **Settings \u2192 AI Assistant \u2192 Knowledge**, you can see which articles the AI currently has access to and test specific questions to see which article it would use.
|
|
350
|
-
|
|
351
|
-
## Syncing after changes
|
|
352
|
-
|
|
353
|
-
The AI index updates within **2 minutes** of publishing, editing, or archiving an article. There is no manual sync required.`, headings: [{ level: 2, text: "How the AI uses articles" }, { level: 2, text: "Writing articles the AI understands well" }, { level: 2, text: "Controlling AI access per article" }, { level: 2, text: "Checking what the AI knows" }, { level: 2, text: "Syncing after changes" }] }, { id: "ai/mcp-servers", url: "https://kerabie.com/docs/ai/mcp-servers", title: "Connecting MCP servers", description: "Give your AI chatbot the tools of any remote MCP server.", section: "ai", body: "An [MCP](https://modelcontextprotocol.io) server publishes a set of tools in a standard way. Connect one and your AI chatbot can use its tools in a conversation, next to the [HTTP tools](/docs/api-reference/ai-tools) you define yourself. Many products already offer an MCP server (order systems, calendars, CRMs), so you can often add actions without writing any API glue.\n\nLooking for the other direction, letting Claude or Cursor manage your Kerabie account? See the [Kerabie MCP server](/docs/sdks/mcp).\n\n## Connect a server\n\n1. Open **AI Tools** in the dashboard and choose **Add MCP server**.\n2. Enter a name, a **description**, and the server's URL (its *Streamable HTTP* endpoint), plus any authentication it needs: a bearer token, or an API key sent in a header you choose.\n3. Kerabie connects, reads the tools the server offers, and lists them. **None are switched on yet.**\n4. Switch on the tools you want the chatbot to use, and use **Test** to run one before the AI does.\n\nWhen the server adds, renames or removes tools, press **Sync**. Tools you had switched on that the server no longer offers are dropped.\n\n## Write a good description\n\nThe description is how the chatbot tells your servers apart. It is shown to the AI with every tool the server offers, so say **what the server is for** and **when to use it**:\n\n> Order and stock lookups for our online shop. Use when a customer asks where their order is or whether an item is available.\n\nIt must be 10 to 300 characters. If you connect several servers, make each description say clearly what the others don't cover, so the AI doesn't pick the wrong one.\n\n## How the chatbot uses them\n\n- The chatbot sees each switched-on tool's name and description, led by your server's name and description, and decides when to call it, exactly as with your own tools.\n- Inputs that are objects or arrays are asked for as JSON and converted back before the call.\n- A tool's text answer is shown to the AI (cut at 8,000 characters). If the tool reports an error or the server is slow to answer (10 seconds), the AI is told the request failed.\n- Every call is made fresh: Kerabie connects, calls the tool and disconnects.\n\n## Limits and safety\n\n- **Plan limit.** Every switched-on MCP tool counts towards your plan's AI tool limit, together with your own tools. If you downgrade, the chatbot keeps using your own tools first, then MCP tools, up to the new limit.\n- **Up to 5 servers** per organisation, and up to 100 tools read from each.\n- **Public addresses only.** Servers on `localhost`, private networks or cloud metadata addresses are rejected, and redirects are not followed.\n- **Secrets are write-only.** Tokens and API-key values are never shown again after you save them. Leave the field blank when editing to keep the stored one.\n- **Trust.** The chatbot will call whatever you switch on, with details a customer gives it. Only connect servers you trust, and switch on the tools you actually want.\n\n## From code\n\n```typescript\nconst { server } = await kerabie.mcpServers.create({\n name: 'Shop tools',\n description: 'Order and stock lookups for our shop. Use when a customer asks about an order or an item.',\n url: 'https://mcp.yourstore.com/mcp',\n authType: 'bearer',\n authConfig: { token: process.env.SHOP_MCP_TOKEN },\n});\n\nawait kerabie.mcpServers.update(server.id, { enabledTools: ['check_stock'] });\nawait kerabie.mcpServers.test(server.id, 'check_stock', { sku: 'A-100' });\n```\n\n| Method | Route | Description |\n|---|---|---|\n| `list()` | `GET /v1/mcp-servers` | Servers, their tools and which are switched on. |\n| `create(params)` | `POST /v1/mcp-servers` | Connect a server. `400` if it can't be reached, `403` if your plan has no AI tools, `409` if already connected. |\n| `sync(id)` | `POST /v1/mcp-servers/:id/sync` | Read the tools again. |\n| `update(id, params)` | `PATCH /v1/mcp-servers/:id` | `name`, `description`, `enabled`, `enabledTools`, `authType`, `authConfig`. `403` if switching tools on would exceed your plan. |\n| `delete(id)` | `DELETE /v1/mcp-servers/:id` | Disconnect and remove. |\n| `test(id, tool, args)` | `POST /v1/mcp-servers/:id/test` | Run one tool once. Limited to 40 a hour (`429`). |", headings: [{ level: 2, text: "Connect a server" }, { level: 2, text: "Write a good description" }, { level: 2, text: "How the chatbot uses them" }, { level: 2, text: "Limits and safety" }, { level: 2, text: "From code" }] }, { id: "ai/overview", url: "https://kerabie.com/docs/ai/overview", title: "AI Features", description: "Let AI handle common questions automatically using your knowledge base.", section: "ai", body: `Kerabie's AI chatbot answers visitor questions instantly using your knowledge base as its source of truth. When it can't answer confidently, it escalates to a human agent.
|
|
354
|
-
|
|
355
|
-
## How it works
|
|
356
|
-
|
|
357
|
-
1. A visitor sends a message
|
|
358
|
-
2. The AI searches your knowledge base for relevant articles
|
|
359
|
-
3. If confidence is high enough, it replies immediately
|
|
360
|
-
4. If not, it hands off to an available agent with context on what was asked
|
|
361
|
-
|
|
362
|
-
The AI never makes up answers \u2014 it only responds based on content you've added.
|
|
363
|
-
|
|
364
|
-
## What the AI can do
|
|
365
|
-
|
|
366
|
-
- Answer FAQs instantly, 24/7
|
|
367
|
-
- Handle multiple conversations simultaneously
|
|
368
|
-
- Collect visitor details before escalating (name, email, order number)
|
|
369
|
-
- Suggest reply drafts to agents while they type
|
|
370
|
-
- Summarise long conversations for agents joining mid-thread
|
|
371
|
-
|
|
372
|
-
## Setup overview
|
|
373
|
-
|
|
374
|
-
<Step title="Add knowledge base articles">
|
|
375
|
-
Go to **Knowledge Base \u2192 Add article** or import from a URL. See [Knowledge Base](/docs/knowledge-base/overview).
|
|
376
|
-
</Step>
|
|
377
|
-
<Step title="Enable AI in widget settings">
|
|
378
|
-
Go to **Settings \u2192 AI \u2192 Enable AI chatbot** and toggle it on for your widget.
|
|
379
|
-
</Step>
|
|
380
|
-
<Step title="Set escalation threshold">
|
|
381
|
-
Choose how confident the AI needs to be before it replies (default: 70%). Below this, it escalates to a human.
|
|
382
|
-
</Step>
|
|
383
|
-
<Step title="Test it">
|
|
384
|
-
Open your widget and ask a question that's covered in your knowledge base. The AI should reply immediately.
|
|
385
|
-
</Step>
|
|
386
|
-
|
|
387
|
-
## AI usage limits
|
|
388
|
-
|
|
389
|
-
| Plan | AI chats/month |
|
|
390
|
-
|---|---|
|
|
391
|
-
| Free | \u2014 |
|
|
392
|
-
| Starter | 200 |
|
|
393
|
-
| Pro | 2,000 |
|
|
394
|
-
| Business | 10,000 |
|
|
395
|
-
|
|
396
|
-
An "AI chat" counts each time the AI sends a reply. Escalations don't count once a human takes over.`, headings: [{ level: 2, text: "How it works" }, { level: 2, text: "What the AI can do" }, { level: 2, text: "Setup overview" }, { level: 2, text: "AI usage limits" }] }, { id: "ai/setup", url: "https://kerabie.com/docs/ai/setup", title: "AI Setup", description: "Enable and configure the AI chatbot for your widget.", section: "ai", body: `Kerabie's AI assistant can answer visitor questions automatically, suggest replies to agents, and escalate to a human when it can't help. It's powered by your knowledge base, so the quality of your articles directly affects AI accuracy.
|
|
397
|
-
|
|
398
|
-
## Enabling the AI assistant
|
|
399
|
-
|
|
400
|
-
1. Go to **Settings \u2192 AI Assistant**
|
|
401
|
-
2. Toggle **Enable AI** on
|
|
402
|
-
3. Choose a mode:
|
|
403
|
-
- **Auto-reply** \u2014 AI responds to visitors automatically before any agent joins
|
|
404
|
-
- **Suggestions only** \u2014 AI drafts replies but an agent must approve and send them
|
|
405
|
-
4. Save
|
|
406
|
-
|
|
407
|
-
## Connecting the knowledge base
|
|
408
|
-
|
|
409
|
-
The AI reads all **published** articles in your knowledge base to answer questions. No additional configuration is needed \u2014 it updates automatically when you publish or edit articles.
|
|
410
|
-
|
|
411
|
-
For best results:
|
|
412
|
-
- Publish articles for your most common questions first
|
|
413
|
-
- Keep articles concise and factual
|
|
414
|
-
- Use plain language \u2014 avoid jargon the AI might misinterpret
|
|
415
|
-
|
|
416
|
-
See [AI & Knowledge Base](/docs/ai/knowledge-base) for tips on writing AI-friendly content.
|
|
417
|
-
|
|
418
|
-
## Configuring the AI persona
|
|
419
|
-
|
|
420
|
-
1. Go to **Settings \u2192 AI Assistant \u2192 Persona**
|
|
421
|
-
2. Set a **bot name** (shown to visitors, e.g. "Aria" or "Support Bot")
|
|
422
|
-
3. Write a **personality prompt** \u2014 describe the tone and style (e.g. "Friendly and concise. Always offer to connect with a human if unsure.")
|
|
423
|
-
4. Save
|
|
424
|
-
|
|
425
|
-
## Escalation to human
|
|
426
|
-
|
|
427
|
-
The AI automatically offers to connect the visitor to a live agent when:
|
|
428
|
-
- It cannot find a relevant answer in the knowledge base
|
|
429
|
-
- The visitor asks to speak with a human
|
|
430
|
-
- Confidence in the answer is below the threshold you set
|
|
431
|
-
|
|
432
|
-
Set the escalation threshold under **Settings \u2192 AI Assistant \u2192 Escalation**.
|
|
433
|
-
|
|
434
|
-
## Testing the AI
|
|
435
|
-
|
|
436
|
-
Use the **Preview** button in AI settings to open a test widget and try questions before going live. Responses are generated in real time using your current knowledge base.
|
|
437
|
-
|
|
438
|
-
## Usage and limits
|
|
439
|
-
|
|
440
|
-
AI responses count against your **AI message quota** \u2014 shown in **Settings \u2192 Usage**. Quotas reset monthly. Upgrade your plan to increase the limit.`, headings: [{ level: 2, text: "Enabling the AI assistant" }, { level: 2, text: "Connecting the knowledge base" }, { level: 2, text: "Configuring the AI persona" }, { level: 2, text: "Escalation to human" }, { level: 2, text: "Testing the AI" }, { level: 2, text: "Usage and limits" }] }, { id: "ai/suggestions", url: "https://kerabie.com/docs/ai/suggestions", title: "AI Reply Suggestions", description: "Let AI draft replies as agents type, for faster and more consistent responses.", section: "ai", body: "AI Reply Suggestions surface a ready-to-send response at the top of the agent's reply box whenever a visitor message comes in. The agent can accept it as-is, edit it, or ignore it.\n\n## Enabling suggestions\n\n1. Go to **Settings \u2192 AI Assistant**\n2. Set mode to **Suggestions only** (or **Auto-reply** \u2014 suggestions appear in both modes)\n3. Save\n\nSuggestions appear automatically in every conversation once enabled \u2014 no per-conversation setup needed.\n\n## How it works for agents\n\nWhen a visitor sends a message, a **suggested reply** card appears above the reply input:\n\n- Click **Use** to copy it into the reply box (you can still edit before sending)\n- Click **Dismiss** to hide the suggestion for that message\n- Ignore it and type your own reply \u2014 the suggestion disappears when you start typing\n\n## Suggestion quality\n\nSuggestions are generated from your knowledge base. The more thorough your articles, the better the suggestions. If the AI can't find relevant content, no suggestion is shown \u2014 agents won't see a low-confidence guess.\n\n## Tracking suggestion usage\n\nUnder **Analytics \u2192 AI**, see:\n- **Suggestion rate** \u2014 what % of messages received a suggestion\n- **Acceptance rate** \u2014 what % of suggestions were used (accepted) by agents\n- **Edit rate** \u2014 what % of accepted suggestions were edited before sending\n\nHigh edit rates on a particular topic mean the suggestion quality is low there \u2014 consider improving the knowledge base articles for that topic.\n\n## Disabling per conversation\n\nAgents can turn off suggestions for a specific conversation by clicking the **AI** toggle in the conversation toolbar. This is useful for sensitive or complex cases where human judgement is needed throughout.", headings: [{ level: 2, text: "Enabling suggestions" }, { level: 2, text: "How it works for agents" }, { level: 2, text: "Suggestion quality" }, { level: 2, text: "Tracking suggestion usage" }, { level: 2, text: "Disabling per conversation" }] }, { id: "analytics/agents", url: "https://kerabie.com/docs/analytics/agents", title: "Agent Performance", description: "See how each agent is performing across response time, volume, and satisfaction.", section: "analytics", body: "The Agent Performance dashboard lets admins compare output and quality across individual agents and teams.\n\n## Accessing agent analytics\n\nGo to **Analytics \u2192 Agents** in the sidebar.\n\n## Metrics per agent\n\n| Metric | Description |\n|---|---|\n| **Conversations handled** | Total conversations the agent replied to in the period |\n| **First response time** | Average time from conversation assignment to first reply |\n| **Resolution rate** | % of their conversations that were resolved |\n| **Avg handle time** | Average duration from first reply to resolution |\n| **CSAT score** | Average customer satisfaction rating received |\n| **Online time** | Hours the agent was set to Available |\n\n## Leaderboard view\n\nSwitch to **Leaderboard** to rank all agents by any metric. Useful for performance reviews or spotting who needs support.\n\n## Team rollup\n\nSelect a **team** from the filter to see aggregated metrics for that team alongside individual agent breakdowns within it.\n\n## Tracking over time\n\nThe **Trend** chart for each agent shows their first response time and CSAT score over the selected period. A rising first response time or falling CSAT score is an early warning sign.\n\n## Using data in 1-on-1s\n\nClick any agent's name to open their detail view showing:\n- Daily conversation volume\n- Response time distribution (fastest / slowest / median)\n- CSAT comments from that period\n- Conversations flagged or escalated\n\nThis view is designed to be used directly in agent performance conversations.\n\n## Date range and filters\n\nAll metrics respect the **date range** selector at the top. You can compare any two periods using the **Compare** toggle.", headings: [{ level: 2, text: "Accessing agent analytics" }, { level: 2, text: "Metrics per agent" }, { level: 2, text: "Leaderboard view" }, { level: 2, text: "Team rollup" }, { level: 2, text: "Tracking over time" }, { level: 2, text: "Using data in 1-on-1s" }, { level: 2, text: "Date range and filters" }] }, { id: "analytics/conversations", url: "https://kerabie.com/docs/analytics/conversations", title: "Conversation Analytics", description: "Track volume, response times, and resolution rates.", section: "analytics", body: "The Conversations analytics dashboard gives you a high-level view of how your support operation is performing over any time range.\n\n## Accessing conversation analytics\n\nGo to **Analytics \u2192 Conversations** in the sidebar.\n\n## Metrics explained\n\n### Volume\n\n| Metric | Description |\n|---|---|\n| **Total conversations** | All conversations started in the period |\n| **New conversations** | Conversations with no prior history from that visitor |\n| **Reopened** | Resolved conversations that received a new message |\n\n### Response time\n\n| Metric | Description |\n|---|---|\n| **First response time** | Time from visitor's first message to agent's first reply |\n| **Average handle time** | Total time a conversation was open (first message to resolution) |\n| **Median response time** | Middle value \u2014 less skewed by outliers than average |\n\n### Resolution\n\n| Metric | Description |\n|---|---|\n| **Resolution rate** | % of conversations marked resolved |\n| **Resolved this period** | Count of conversations resolved |\n| **Unresolved** | Still open at end of period |\n\n## Charts\n\nThe **Volume over time** chart shows daily conversation counts. Toggle between:\n- Total conversations\n- Resolved conversations\n- Unresolved conversations\n\nThe **Response time trend** chart shows how first response time has changed day by day \u2014 useful for spotting regressions after team changes.\n\n## Filtering\n\nFilter all metrics by:\n- **Date range** \u2014 today, last 7/30/90 days, or custom\n- **Team** \u2014 see metrics for a specific team only\n- **Agent** \u2014 drill down to an individual's numbers\n- **Channel** \u2014 widget, API, or specific integration\n\n## Exporting\n\nClick **Export \u2192 CSV** to download a row-per-conversation report. See [History & Export](/docs/conversations/history-export) for the full field reference.", headings: [{ level: 2, text: "Accessing conversation analytics" }, { level: 2, text: "Metrics explained" }, { level: 3, text: "Volume" }, { level: 3, text: "Response time" }, { level: 3, text: "Resolution" }, { level: 2, text: "Charts" }, { level: 2, text: "Filtering" }, { level: 2, text: "Exporting" }] }, { id: "analytics/partner-program", url: "https://kerabie.com/docs/analytics/partner-program", title: "Partner Program", description: "Earn commission by referring businesses to Kerabie.", section: "analytics", body: "The Kerabie Partner Program lets bloggers, agencies, and influencers earn commission by referring new businesses to Kerabie.\n\n## Commission structure\n\n- **20%** recurring commission for every paying customer you refer\n- Commission is paid monthly after the referred customer's subscription is confirmed\n- Minimum payout: \u20A65,000\n- Payouts via bank transfer (Nigeria) or wire transfer (international)\n\n## Getting started\n\n1. Go to **Partner Dashboard** in your Kerabie account (or via the sidebar)\n2. Click **Apply to Partner Program**\n3. Enter your brand name, website, and how you plan to promote Kerabie\n4. Choose a custom referral code (or auto-generate one)\n5. Submit for review \u2014 approval takes 1\u20132 business days\n\n## Sharing your link\n\nOnce approved, your referral link is:\n```\nhttps://kerabie.com/?ref=YOURCODE\n```\n\nShare it in:\n- Blog posts and reviews\n- YouTube video descriptions\n- Email newsletters\n- Social media bios\n\nAnyone who clicks your link and signs up is tagged to your account. If they subscribe to a paid plan, you earn commission.\n\n## Partner dashboard\n\nYour dashboard shows:\n- **Total clicks** \u2014 how many people clicked your referral link\n- **Signups** \u2014 how many signed up using your link\n- **Conversion rate** \u2014 signups \xF7 clicks\n- **Earnings** \u2014 per-referral breakdown (pending, approved, paid)\n- **Payouts** \u2014 your withdrawal history\n\n## Requesting a payout\n\n1. Go to **Partner Dashboard \u2192 Payout**\n2. Add your bank details (one-time setup)\n3. Enter the amount to withdraw\n4. Submit \u2014 processed within 2\u20133 business days\n\n## API access\n\nSee the [Partner API reference](/docs/api-reference/growth) to programmatically access your referral stats, earnings, and payout history.", headings: [{ level: 2, text: "Commission structure" }, { level: 2, text: "Getting started" }, { level: 2, text: "Sharing your link" }, { level: 2, text: "Partner dashboard" }, { level: 2, text: "Requesting a payout" }, { level: 2, text: "API access" }] }, { id: "analytics/satisfaction", url: "https://kerabie.com/docs/analytics/satisfaction", title: "Satisfaction Scores", description: "Collect and analyse CSAT ratings from visitors after conversations.", section: "analytics", body: 'Kerabie sends an automatic satisfaction survey after a conversation is resolved. The results feed into the **Satisfaction** analytics dashboard.\n\n## Enabling CSAT surveys\n\n1. Go to **Settings \u2192 Widget Settings \u2192 Satisfaction**\n2. Toggle **Send CSAT survey after resolution** on\n3. Customise the survey message (optional)\n4. Save\n\nThe survey appears in the widget a few minutes after an agent resolves the conversation.\n\n## Survey format\n\nThe default survey asks:\n\n> "How would you rate your support experience?"\n> \u2B50 \u2B50 \u2B50 \u2B50 \u2B50 (1\u20135 stars)\n\nAn optional follow-up text field collects written comments. Both are shown in the satisfaction dashboard.\n\n## Metrics\n\n### Overall CSAT\n\n| Metric | Description |\n|---|---|\n| **CSAT score** | Average rating across all responses in the period (out of 5) |\n| **Response rate** | % of resolved conversations where the visitor submitted a rating |\n| **Total responses** | Raw count of ratings received |\n\n### Distribution\n\nThe **Rating breakdown** chart shows the count of 1-star through 5-star ratings. A large number of 1\u20132 star ratings with comments is the most actionable signal.\n\n## Per-agent CSAT\n\nRatings are linked to the agent who resolved the conversation, so you can compare CSAT scores per agent in **Analytics \u2192 Agents**.\n\n## Comments\n\nAll written comments are listed in the **Comments** tab, newest first. Filter by:\n- Rating (e.g. only 1-star comments)\n- Agent\n- Date range\n\nUse low-rating comments directly in agent coaching sessions.\n\n## Improving your score\n\nCommon causes of low CSAT:\n- Slow first response time \u2014 address with automations and staffing\n- Unresolved issues \u2014 check resolution rate alongside CSAT\n- Tone \u2014 review low-rated transcripts to spot patterns in how agents communicate', headings: [{ level: 2, text: "Enabling CSAT surveys" }, { level: 2, text: "Survey format" }, { level: 2, text: "Metrics" }, { level: 3, text: "Overall CSAT" }, { level: 3, text: "Distribution" }, { level: 2, text: "Per-agent CSAT" }, { level: 2, text: "Comments" }, { level: 2, text: "Improving your score" }] }, { id: "api-reference/agents", url: "https://kerabie.com/docs/api-reference/agents", title: "Agents", description: "List and retrieve agents in your organisation.", section: "api-reference", body: '## List agents\n\n```http\nGET /agents\n```\n\nReturns all agents who have access to your organisation.\n\n**Response**\n\n```json\n{\n "data": [\n {\n "id": 7,\n "name": "Sarah",\n "email": "sarah@yourcompany.com",\n "availability": "online",\n "role": "agent",\n "avatar": "https://assets.kerabie.com/avatars/7.png"\n }\n ]\n}\n```\n\n---\n\n## Get an agent\n\n```http\nGET /agents/:id\n```\n\n---\n\n## Agent availability\n\nAgents set their own availability from the dashboard or mobile app. The `availability` field is one of:\n\n| Value | Meaning |\n|---|---|\n| `online` | Available to receive conversations |\n| `away` | Temporarily unavailable |\n| `busy` | In a conversation, prefer not to be assigned |\n| `offline` | Not logged in or signed out |', headings: [{ level: 2, text: "List agents" }, { level: 2, text: "Get an agent" }, { level: 2, text: "Agent availability" }] }, { id: "api-reference/ai-tools", url: "https://kerabie.com/docs/api-reference/ai-tools", title: "AI Tools API", description: "Let your AI chatbot call your own APIs, and manage those tools from code.", section: "api-reference", body: 'These endpoints use your [API key](/docs/api-reference/authentication) and manage the same tools as the **AI Tools** page in the dashboard.\n\n```typescript\n\nconst kerabie = new KerAbieClient({ apiKey: process.env.KERABIE_API_KEY });\n```\n\nA **tool** is an API of yours that the chatbot may call mid-conversation: check stock, validate an address, look up an order, book a slot. The AI reads the tool\'s **description** to decide when to call it, collects the **parameters** from the customer, calls your endpoint, and uses the answer in its reply.\n\nThree built-in tools are always available and need no setup: `save_memory`, `get_memory` and `escalate_to_agent`.\n\n## How a call works\n\n| Method | What is sent |\n|---|---|\n| `GET` | The parameters in the query string. |\n| `POST`, `PUT`, `PATCH` | The parameters as a JSON body. |\n\nYour endpoint has 8 seconds to answer. If you set a `responsePath` such as `data.message`, only that part of a JSON reply is shown to the AI; otherwise it sees the whole reply (cut at 8,000 characters). A non-2xx answer, a timeout or a network error is reported to the AI as a failure, and it will say it couldn\'t complete the request.\n\n## Rules\n\n- **Public addresses only.** Endpoints on `localhost`, private networks or cloud metadata addresses are rejected, and redirects are not followed. Use the final URL.\n- **Secrets are write-only.** `authConfig` values are stored when you send them and never returned. On `update`, leave a secret out (or blank) to keep the stored one.\n- **Plan limit.** Your plan sets how many tools you can have. `stats.limit` is `-1` when AI tools aren\'t on your plan and `0` when unlimited. `create` and `duplicate` return **403** when it is full. If you downgrade, the chatbot keeps using your oldest enabled tools up to the new limit.\n- **Function names** are snake_case and unique in your organisation (**409** if taken).\n- Up to **20 parameters** and **10 custom headers** per tool.\n\n---\n\n## List tools\n\n```http\nGET /v1/ai-tools\n```\n\n```typescript\nconst { tools, stats } = await kerabie.aiTools.list();\n```\n\n```json\n{\n "tools": [\n {\n "id": 1,\n "name": "check_availability",\n "displayName": "Check availability",\n "description": "Use this when a customer asks if a specific item is in stock.",\n "parameters": [\n { "name": "sku", "type": "string", "description": "Product SKU the customer mentioned", "required": true }\n ],\n "endpoint": "https://api.yourstore.com/inventory/check",\n "method": "GET",\n "authType": "bearer",\n "responsePath": "data.message",\n "enabled": true,\n "createdAt": "2026-10-01T09:00:00.000Z",\n "updatedAt": "2026-10-01T09:00:00.000Z",\n "usage": { "calls": 142, "failures": 6, "lastUsedAt": "2026-10-06T12:30:00.000Z" }\n }\n ],\n "stats": { "total": 1, "active": 1, "calls": 142, "successRate": 96, "limit": 5 }\n}\n```\n\n`usage` and `stats.calls` cover the last 30 days. `successRate` is `null` when there were no calls.\n\n## Get a tool\n\n```http\nGET /v1/ai-tools/:id\n```\n\nReturns the tool with `authConfig` (names only, secrets come back empty), `headers` and `hasSecret`.\n\n## Create a tool\n\n```http\nPOST /v1/ai-tools\n```\n\n```typescript\nconst { tool } = await kerabie.aiTools.create({\n name: \'check_availability\',\n displayName: \'Check availability\',\n description: \'Use this when a customer asks if a specific item is in stock.\',\n endpoint: \'https://api.yourstore.com/inventory/check\',\n method: \'GET\',\n authType: \'bearer\',\n authConfig: { token: process.env.STORE_API_TOKEN },\n responsePath: \'data.message\',\n parameters: [{ name: \'sku\', type: \'string\', description: \'Product SKU the customer mentioned\' }],\n});\n```\n\n| Field | Description |\n|---|---|\n| `name` | Required. snake_case, 2-60 characters. What the AI calls. |\n| `displayName` | Required. 2-80 characters. |\n| `description` | Required. 10-500 characters. Say **when** the AI should use the tool ("Use this when a customer asks\u2026"). |\n| `endpoint` | Required. A public `https://` (or `http://`) address. |\n| `method` | `GET`, `POST` (default), `PUT` or `PATCH`. |\n| `parameters` | List of `{ name, type, description?, required? }`. `type` is `string`, `number` or `boolean`; `required` defaults to `true`. |\n| `authType` | `none` (default), `bearer`, `api_key` or `basic`. |\n| `authConfig` | `bearer`: `{ token }` \xB7 `api_key`: `{ header, value }` \xB7 `basic`: `{ username, password }`. |\n| `headers` | Extra static headers sent on every call. |\n| `responsePath` | Dot-notation path into the JSON reply, e.g. `data.message`. |\n| `enabled` | Default `true`. |\n\nReturns `{ "tool": { "id": 12 } }`.\n\n## Update a tool\n\n```http\nPATCH /v1/ai-tools/:id\n```\n\n```typescript\nawait kerabie.aiTools.update(12, { enabled: false });\n```\n\nSend only what you want to change. Changing `authType` needs the new secrets in `authConfig`.\n\n## Duplicate a tool\n\n```http\nPOST /v1/ai-tools/:id/duplicate\n```\n\nCreates a copy named `<name>_copy` that starts **switched off**, so you can edit it before the AI can use it.\n\n## Delete a tool\n\n```http\nDELETE /v1/ai-tools/:id\n```\n\n## Test a tool\n\n```http\nPOST /v1/ai-tools/:id/test\n```\n\n```typescript\nconst result = await kerabie.aiTools.test(12, { sku: \'A-100\' });\n// { success: true, result: \'In stock: 14 units\', statusCode: 200, durationMs: 184 }\n```\n\nCalls your real endpoint once with `args`. It is not counted in `usage`, and is limited to 40 tests per hour per organisation (**429**).\n\n## Errors\n\n| Status | When |\n|---|---|\n| `400` | Invalid input, or an endpoint on a private or local address. |\n| `403` | AI tools aren\'t on your plan, or your plan\'s tool limit is reached. |\n| `404` | No such tool in your organisation. |\n| `409` | A tool with that function name already exists. |\n| `429` | Test limit reached. |', headings: [{ level: 2, text: "How a call works" }, { level: 2, text: "Rules" }, { level: 2, text: "List tools" }, { level: 2, text: "Get a tool" }, { level: 2, text: "Create a tool" }, { level: 2, text: "Update a tool" }, { level: 2, text: "Duplicate a tool" }, { level: 2, text: "Delete a tool" }, { level: 2, text: "Test a tool" }, { level: 2, text: "Errors" }] }, { id: "api-reference/analytics", url: "https://kerabie.com/docs/api-reference/analytics", title: "Analytics API", description: "Conversation, AI, ticket and team numbers \u2014 the same data that powers your dashboard.", section: "api-reference", body: 'All analytics endpoints use your [API key](/docs/api-reference/authentication) and return the same data, from the same cache, as the Kerabie dashboard. Results are cached briefly (summary and agents: 5 minutes; tickets and setup: 1 minute), so a number can lag live activity slightly.\n\n```typescript\n\nconst kerabie = new KerAbieClient({ apiKey: process.env.KERABIE_API_KEY });\n```\n\n## Summary\n\n```http\nGET /v1/analytics/summary\n```\n\n| Query param | Description |\n|---|---|\n| `days` | Window length in days. Default `30`, maximum `365`. |\n| `period` | Alternative to `days`, e.g. `7d` or `30d`. Wins if both are sent. |\n\n```typescript\nconst summary = await kerabie.analytics.getSummary({ days: 7 });\n```\n\n**Response**\n\n```json\n{\n "period": { "days": 7, "since": "2026-09-28T00:00:00.000Z" },\n "openConversations": 14,\n "resolvedToday": 9,\n "avgResponseTime": 144,\n "satisfactionScore": 96,\n "activeVisitors": 23,\n "totals": { "conversations": 186, "resolved": 120, "closed": 20, "messages": 900, "visitors": 40, "aiChats": 310, "tickets": 22, "contacts": 35, "siteVisitors": 1290 },\n "rates": { "resolution": 75 },\n "satisfaction": { "avgRating": 4.8, "totalRatings": 30 },\n "deltas": { "conversations": 12, "resolved": 9, "responseTime": -8, "satisfaction": 4, "aiChats": 31 },\n "ai": { "resolvedAlone": 130, "resolvedAlonePct": 70, "handedOffPct": 18, "agentHoursSaved": 41 },\n "channels": [{ "channel": "WHATSAPP", "count": 54 }, { "channel": "WEBSITE", "count": 38 }],\n "aiEscalations": [{ "reason": "kb_gap", "count": 33 }],\n "trend": [\n { "date": "2026-10-04", "count": 28, "ai": 17, "human": 11 },\n { "date": "2026-10-05", "count": 24, "ai": 15, "human": 9 }\n ]\n}\n```\n\n**Field notes**\n\n- `avgResponseTime` is the average first-response time in **seconds** (`0` when there is no data yet).\n- `satisfactionScore` is the average rating \xD7 20, so `0\u2013100`.\n- `deltas` are the **percentage change versus the previous period of the same length**. A value is `null` when there is nothing to compare against. For `responseTime`, a **negative** number is an improvement.\n- `ai.resolvedAlone` counts conversations the AI handled without a human ever replying. `ai.handedOffPct` is the share handed off to a person.\n- `ai.agentHoursSaved` is an **estimate**: AI-only conversations \xD7 the average time your agents spent on the human-handled ones in the same window (each clamped to 1\u2013120 minutes; 8 minutes is assumed when there are no human-handled conversations yet).\n- `trend` has one entry per day, oldest first, capped at the last 30 days, with empty days included. `ai + human = count`.\n\n---\n\n## Insights\n\n```http\nGET /v1/analytics/insights\n```\n\nTakes the same `days` / `period` parameters as the summary. This is what powers the **Agents**, **Channels**, **AI and questions**, **Customers** and **Pages** tabs of the dashboard\'s Analytics page.\n\n```typescript\nconst insights = await kerabie.analytics.getInsights({ days: 30 });\n```\n\n```json\n{\n "period": { "days": 30, "since": "2026-09-06T00:00:00.000Z" },\n "firstResponse": { "medianSeconds": 144, "deltaPct": -8, "sample": 240 },\n "agents": [\n { "agentId": 1, "name": "Amara Osei", "availability": "online", "messages": 900, "assigned": 142, "resolved": 134, "resolutionRate": 94, "avgRating": 4.8, "firstResponseSeconds": 126 }\n ],\n "channels": [\n { "channel": "WHATSAPP", "count": 108, "pct": 38, "firstResponseSeconds": 132, "avgRating": 4.7 }\n ],\n "ai": {\n "csatOnAiChats": { "avg": 4.4, "count": 61 },\n "questions": [\n { "question": "Where is my order?", "asked": 128, "aiResolvedPct": 74, "avgRating": 4.1, "page": "/orders", "trendPct": 6 }\n ],\n "unanswered": [{ "question": "Do you support SSO with Okta?", "count": 23 }],\n "outcomes": { "resolvedByAi": 198, "handedOff": 51, "other": 35 }\n },\n "customers": {\n "active": 1230, "new": 412, "returning": 818, "perCustomer": 2.3,\n "deltas": { "active": 23, "new": 14, "returning": 27 },\n "csat": [{ "stars": 5, "count": 111, "pct": 68 }, { "stars": 4, "count": 33, "pct": 20 }],\n "csatResponses": 163,\n "countries": [{ "country": "NG", "count": 412 }],\n "newPct": 34, "returningPct": 66,\n "top": [{ "id": 1, "name": "Chidi Okafor", "company": "Palmex", "conversations": 14, "lastSeenAt": "2026-10-06T10:30:00.000Z", "avgRating": 4.2 }],\n "heatmap": { "days": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], "cells": [[0, 1, 0, 2, 5, 9, 14, 12, 8, 4, 2, 0]], "timezone": "Africa/Lagos" }\n },\n "pages": [\n { "page": "/pricing", "views": 4210, "questions": 412, "askRate": 9.8, "topQuestion": "Can I get a refund after 30 days?" }\n ]\n}\n```\n\n**Field notes**\n\n- `firstResponse.medianSeconds` is the **median** time from a customer\'s first message to a person\'s first reply (AI replies don\'t count). `deltaPct` compares with the previous period; negative is faster.\n- Questions are grouped by their **first customer message**, ignoring case and punctuation. Greetings ("hi", "thanks") are left out. Only the top 20 are returned, most asked first.\n- `aiResolvedPct` is the share of those conversations where no person ever replied and the AI didn\'t hand off. `avgRating` is the customers\' own 1\u20135 rating of those conversations.\n- `unanswered` are the questions behind AI handoffs with the reason *no matching knowledge base article*: good candidates for new articles.\n- `heatmap.cells` has one row per weekday (Monday first) and 12 columns, each a two-hour block starting at 0, 2, \u2026 22, counted in the organisation\'s own timezone.\n- `pages`: `views` are the widget page views we recorded (kept for 90 days) and `questions` the conversations that started on that page. `askRate` = questions \xF7 views, and is `null` while a page has no recorded views. A conversation\'s page is the one the visitor was on when they wrote in; conversations before this was tracked have none.\n- Computed from up to the **20,000 most recent conversations** in the window and cached for up to 5 minutes.\n\n---\n\n## Agent performance\n\n```http\nGET /v1/analytics/agents\n```\n\nAccepts the same `days` / `period` parameters as the summary.\n\n```typescript\nconst { agents } = await kerabie.analytics.getAgentStats({ period: \'7d\' });\n```\n\n```json\n{\n "agents": [\n {\n "agentId": 7,\n "name": "Amara Osei",\n "avatar": "https://cdn.kerabie.com/avatars/7.jpg",\n "availability": "online",\n "messages": 200,\n "assigned": 12,\n "resolved": 10,\n "resolutionRate": 83,\n "avgRating": 4.9\n }\n ]\n}\n```\n\n`name` falls back to the part of the email before the `@` for agents who haven\'t set a name. Agents are sorted by `resolved`, highest first.\n\n---\n\n## Open tickets and SLA\n\n```http\nGET /v1/analytics/tickets-sla\n```\n\n```typescript\nconst sla = await kerabie.analytics.getTicketSla();\n```\n\n```json\n{\n "total": 32,\n "withinSlaPct": 94,\n "byPriority": [\n { "priority": "URGENT", "count": 3, "oldestMinutes": 14 },\n { "priority": "HIGH", "count": 8, "oldestMinutes": 60 },\n { "priority": "NORMAL", "count": 21, "oldestMinutes": 180 },\n { "priority": "LOW", "count": 0, "oldestMinutes": null }\n ]\n}\n```\n\nOnly open tickets (new or active) are counted. `withinSlaPct` is the share that has **not** passed its SLA due time, and is `100` when there are no open tickets. `oldestMinutes` is the age of the oldest ticket at that priority, or `null` if there are none.\n\n---\n\n## Setup status\n\n```http\nGET /v1/analytics/setup-status\n```\n\n```typescript\nconst status = await kerabie.analytics.getSetupStatus();\n// { channel: true, ai: false, automation: false }\n```\n\n| Field | `true` when |\n|---|---|\n| `channel` | WhatsApp or Instagram is connected and active, or an email mailbox is connected. |\n| `ai` | At least one published knowledge-base item exists. |\n| `automation` | At least one automation exists. |', headings: [{ level: 2, text: "Summary" }, { level: 2, text: "Insights" }, { level: 2, text: "Agent performance" }, { level: 2, text: "Open tickets and SLA" }, { level: 2, text: "Setup status" }] }, { id: "api-reference/authentication", url: "https://kerabie.com/docs/api-reference/authentication", title: "Authentication", description: "How to authenticate requests to the Kerabie API.", section: "api-reference", body: "## API Keys\n\nAPI keys are the primary way to authenticate with the Kerabie API. They're scoped per organisation and never expire unless you revoke them.\n\n### Create a key\n\n1. Go to **Settings \u2192 API \u2192 New API Key**\n2. Give it a name (e.g. \"CRM integration\")\n3. Copy the key \u2014 it's shown only once\n\n### Use it in requests\n\n```http\nAuthorization: Bearer ker_live_abc123...\nX-Kerabie-Org: 42\nContent-Type: application/json\n```\n\nOr with the SDK:\n\n```typescript\nconst kerabie = new KerAbieClient({\n apiKey: 'ker_live_abc123...',\n orgId: 42,\n});\n```\n\n## Key prefixes\n\n| Prefix | Environment |\n|---|---|\n| `ker_live_` | Production |\n| `ker_test_` | Test (sandbox data, no real conversations) |\n\n## Revoking keys\n\nGo to **Settings \u2192 API** and click the trash icon next to any key. Revocation is immediate.\n\n## Keeping keys secure\n\n- Store keys in environment variables, never in source code\n- Use separate keys per integration so you can revoke one without affecting others\n- Rotate keys periodically for sensitive integrations\n\n Never commit API keys to version control. If a key is exposed, revoke it immediately and create a new one.", headings: [{ level: 2, text: "API Keys" }, { level: 3, text: "Create a key" }, { level: 3, text: "Use it in requests" }, { level: 2, text: "Key prefixes" }, { level: 2, text: "Revoking keys" }, { level: 2, text: "Keeping keys secure" }] }, { id: "api-reference/campaigns", url: "https://kerabie.com/docs/api-reference/campaigns", title: "Campaigns API", description: "Create, launch and measure proactive messages on your chat widget.", section: "api-reference", body: 'These endpoints use your [API key](/docs/api-reference/authentication) and manage the same campaigns as the **Campaigns** page in the dashboard.\n\n```typescript\n\nconst kerabie = new KerAbieClient({ apiKey: process.env.KERABIE_API_KEY });\n```\n\nA **campaign** is a short message (with an optional button) that pops up next to your chat bubble when a visitor meets its rules: a trigger, the pages it applies to, who it is for, and how often.\n\n## Status\n\n| Status | Meaning |\n|---|---|\n| `draft` | Saved but never launched. Visitors don\'t see it. |\n| `active` | Live on your widget. |\n| `paused` | Launched before, switched off now. |\n\nDrafts and paused campaigns are unlimited. Your plan limits how many can be **active** at once: `GET /v1/campaigns/stats` returns it as `limit` (`0` means unlimited).\n\n---\n\n## List campaigns\n\n```http\nGET /v1/campaigns\n```\n\n```typescript\nconst { campaigns, meta } = await kerabie.campaigns.list({ status: \'active\', limit: 20 });\n```\n\n| Query param | Description |\n|---|---|\n| `status` | `active`, `paused` or `draft`. |\n| `search` | Case-insensitive match on name or message. |\n| `page`, `limit` | Pagination. `limit` defaults to 20, maximum 100. |\n\n```json\n{\n "campaigns": [\n {\n "id": 1,\n "name": "Pricing page nudge",\n "message": "Questions about plans? I can compare them for you in two minutes.",\n "ctaLabel": "Compare plans",\n "ctaAction": "open_chat",\n "ctaUrl": null,\n "trigger": "time_on_page",\n "triggerValue": "20",\n "pages": "/pricing",\n "audience": "all",\n "audienceCountry": null,\n "frequency": "once_per_visitor",\n "isActive": true,\n "status": "active",\n "launchedAt": "2026-10-01T09:00:00.000Z",\n "sentCount": 1240,\n "openedCount": 612,\n "clickCount": 188,\n "repliedCount": 74,\n "createdAt": "2026-09-30T14:20:00.000Z"\n }\n ],\n "meta": { "total": 1, "page": 1, "limit": 20, "pages": 1 }\n}\n```\n\nThe four counters are the campaign\'s funnel: **shown** (`sentCount`), **opened** the chat from it (`openedCount`), **pressed its button** (`clickCount`) and **wrote back** (`repliedCount`).\n\n---\n\n## Overview numbers\n\n```http\nGET /v1/campaigns/stats\n```\n\n```json\n{ "total": 4, "active": 2, "paused": 1, "draft": 1, "sent": 2650, "opened": 1323, "clicked": 426, "replied": 153, "limit": 5 }\n```\n\nCached for up to a minute; changes to campaigns show immediately.\n\n---\n\n## Get a campaign\n\n```http\nGET /v1/campaigns/:id\n```\n\n`404` if it isn\'t in your organisation.\n\n---\n\n## Create a campaign\n\n```http\nPOST /v1/campaigns\n```\n\n```typescript\nconst { campaign } = await kerabie.campaigns.create({\n name: \'Checkout rescue\',\n message: \'Need a hand finishing your order? We are online right now.\',\n ctaLabel: \'Chat with us\',\n trigger: \'exit_intent\',\n pages: \'/checkout\',\n audience: \'returning\',\n frequency: \'once_per_day\',\n isActive: true,\n});\n```\n\n| Body field | Notes |\n|---|---|\n| `name` | **Required.** Up to 80 characters. |\n| `message` | **Required.** Up to 500 characters. |\n| `imageUrl` | Read-only cover image shown full-width on top of the popup (set it in the dashboard or the app). Without one, visitors see your logo or initial. |\n| `ctaLabel` | Button text, up to 30 characters. Leave empty for no button. |\n| `ctaAction` | `open_chat` (default), `open_kb` (opens the Help screen) or `open_link`. |\n| `ctaUrl` | Required for `open_link`: a full `http://` or `https://` link. |\n| `trigger` | `page_load` (default), `time_on_page`, `scroll_depth` or `exit_intent`. Exit intent works with a mouse, so it never fires on phones. |\n| `triggerValue` | Whole number. Seconds for `time_on_page` (1\u2013600, default 10); percent for `scroll_depth` (1\u2013100, default 50). Ignored for the other triggers. |\n| `pages` | Comma-separated URL patterns, `*` matches anything: `/pricing`, `/docs*`, `https://shop.com/checkout`. Up to 20. Empty = every page. |\n| `audience` | `all` (default), `new` (first visit), `returning`, `not_chatted` (hasn\'t started a conversation), or `country` (needs `audienceCountry`, e.g. `NG`; matched from the visitor\'s IP address, so visitors whose location can\'t be determined don\'t see it). |\n| `audienceCountry` | Two-letter country code. Required when `audience` is `country`, ignored otherwise. |\n| `frequency` | `once_per_visitor` (default), `once_per_day`, or `every_session` (once per page load). |\n| `isActive` | `true` launches it now. Default: saved as a draft. |\n\n| Status | When |\n|---|---|\n| `400` | A field is missing or invalid; the message says which. |\n| `403` | Launching would exceed your plan\'s live-campaign limit. |\n\n---\n\n## Update a campaign\n\n```http\nPATCH /v1/campaigns/:id\n```\n\nSend only what you want to change. `isActive: true` launches or resumes it (same `403` rule), `isActive: false` pauses it. Pausing is always allowed. Changing `trigger` resets `triggerValue` unless you send a new one.\n\n---\n\n## Duplicate a campaign\n\n```http\nPOST /v1/campaigns/:id/duplicate\n```\n\nReturns a copy named \u201C\u2026 (copy)\u201D saved as a draft, with its results reset to zero.\n\n---\n\n## Delete a campaign\n\n```http\nDELETE /v1/campaigns/:id\n```\n\nThe campaign stops showing and its results are removed.\n\n---\n\n## How visitors see it\n\nThe chat widget loads your live campaigns once per page view and checks each one\'s rules in the visitor\'s browser:\n\n1. The **page** matches one of `pages`.\n2. The **audience** fits: a visitor who was on your site before counts as returning.\n3. The **frequency** allows it, based on when this visitor last saw that campaign.\n4. The **trigger** fires: after N seconds, at N% scroll, when the mouse leaves through the top of the window, or shortly after the page loads.\n\nThe widget reports each step (shown, opened, clicked, replied) to your results. Each event is counted at most once per appearance, and the endpoint is rate limited per IP so counts can\'t be inflated.', headings: [{ level: 2, text: "Status" }, { level: 2, text: "List campaigns" }, { level: 2, text: "Overview numbers" }, { level: 2, text: "Get a campaign" }, { level: 2, text: "Create a campaign" }, { level: 2, text: "Update a campaign" }, { level: 2, text: "Duplicate a campaign" }, { level: 2, text: "Delete a campaign" }, { level: 2, text: "How visitors see it" }] }, { id: "api-reference/contacts", url: "https://kerabie.com/docs/api-reference/contacts", title: "Contacts API", description: "List, add, update and import the people you talk to.", section: "api-reference", body: 'These endpoints use your [API key](/docs/api-reference/authentication) and return the same data as the **Contacts** page in the dashboard.\n\n```typescript\n\nconst kerabie = new KerAbieClient({ apiKey: process.env.KERABIE_API_KEY });\n```\n\nA **contact** is anyone with at least a name, email or phone number. Website visitors who only browsed are not contacts until they message you or you save them.\n\n## List contacts\n\n```http\nGET /v1/contacts\n```\n\n```typescript\nconst { contacts, meta } = await kerabie.contacts.list({ type: \'VIP\', sort: \'recent\' });\n```\n\n| Query param | Description |\n|---|---|\n| `search` | Case-insensitive match on name, email, phone, company or a tag name. |\n| `type` | `LEAD`, `CUSTOMER` or `VIP`. |\n| `tagId` | Only contacts with this tag. |\n| `ownerId` | Only contacts owned by this team member. |\n| `sort` | `recent` (default: last contact first, contacts who have never messaged last), `newest` or `name`. |\n| `page`, `limit` | Pagination. `limit` defaults to 20, maximum 100. |\n\n```json\n{\n "contacts": [\n {\n "id": 24,\n "name": "Chidi Okafor",\n "email": "chidi@palmex.com",\n "phone": "+234 803 555 0142",\n "company": "Palmex",\n "notes": "Prefers WhatsApp.",\n "contactType": "VIP",\n "ownerId": 7,\n "owner": { "id": 7, "user": { "fname": "Amara", "lname": "Osei", "email": "amara@kerabie.com", "avatar": null } },\n "tagLinks": [{ "tag": { "id": 3, "name": "Enterprise", "color": null } }],\n "visitorCountry": "NG",\n "externalSource": null,\n "lastContactAt": "2026-10-05T10:52:00.000Z",\n "createdAt": "2026-09-28T09:10:00.000Z",\n "_count": { "conversations": 3 }\n }\n ],\n "meta": { "total": 9, "page": 1, "limit": 20, "pages": 1 }\n}\n```\n\n`lastContactAt` is the last message in either direction (replies from the AI assistant don\'t count). For contacts created before this was tracked it falls back to their latest conversation.\n\n---\n\n## Overview numbers\n\n```http\nGET /v1/contacts/stats\n```\n\n```json\n{ "total": 9, "newThisWeek": 2, "leads": 2, "vip": 2 }\n```\n\n`newThisWeek` counts contacts added in the last 7 days. Results are cached for up to a minute and refresh immediately when contacts change.\n\n---\n\n## Get a contact\n\n```http\nGET /v1/contacts/:id\n```\n\nReturns the contact with its last 10 conversations. `404` if it isn\'t in your organisation.\n\n---\n\n## Add a contact\n\n```http\nPOST /v1/contacts\n```\n\n```typescript\nconst { contact } = await kerabie.contacts.create({ name: \'Chidi Okafor\', email: \'chidi@palmex.com\', contactType: \'CUSTOMER\' });\n```\n\n| Body field | Notes |\n|---|---|\n| `name`, `email`, `phone` | **At least one is required.** |\n| `company`, `notes` | Optional. Notes up to 5,000 characters. |\n| `contactType` | `LEAD` (default), `CUSTOMER` or `VIP`. |\n| `ownerId` | A team-member id in your organisation, or `null`. |\n\n| Status | When |\n|---|---|\n| `400` | Nothing identifying was given, the email is malformed, the type is unknown, or the owner isn\'t on your team. |\n| `409` | A contact with this email (any casing) already exists. |\n\n---\n\n## Update a contact\n\n```http\nPATCH /v1/contacts/:id\n```\n\nSend only the fields you want to change. A blank text field clears it, and `ownerId: null` removes the owner. The same validation and `409` rule as adding applies: you can\'t give a contact an email another contact already has.\n\n---\n\n## Import contacts\n\n```http\nPOST /v1/contacts/import\n```\n\n```typescript\nconst result = await kerabie.contacts.import([\n { name: \'Sam Adeyemi\', email: \'sam@tenda.ng\', company: \'Tenda\', type: \'lead\' },\n { email: \'not-an-email\' },\n]);\n```\n\n```json\n{ "created": 1, "duplicates": 0, "invalid": [{ "row": 2, "error": "\\"not-an-email\\" is not a valid email" }], "total": 2 }\n```\n\n- Up to **1,000 rows** per call.\n- Each row needs a name or an email. `type` is `lead`, `customer` or `vip` (any case) and defaults to lead.\n- Rows whose email already belongs to a contact are skipped and counted in `duplicates`. The same email twice in one request is reported as invalid on the second row.\n- One bad row never fails the others; invalid rows are listed with their number (starting at 1).', headings: [{ level: 2, text: "List contacts" }, { level: 2, text: "Overview numbers" }, { level: 2, text: "Get a contact" }, { level: 2, text: "Add a contact" }, { level: 2, text: "Update a contact" }, { level: 2, text: "Import contacts" }] }, { id: "api-reference/conversations", url: "https://kerabie.com/docs/api-reference/conversations", title: "Conversations", description: "List, retrieve, and manage conversations via the API.", section: "api-reference", body: '## List conversations\n\n```http\nGET /v1/conversations\n```\n\n```typescript\nconst { data, meta } = await kerabie.conversations.list({ status: \'WAITING\', limit: 20 });\n```\n\n**Query params**\n\n| Param | Type | Description |\n|---|---|---|\n| `status` | `WAITING \\| ACTIVE \\| RESOLVED \\| CLOSED` | Filter by status (`WAITING` is shown as **New** in the dashboard) |\n| `channel` | `string` | Filter by channel, e.g. `WHATSAPP` |\n| `assignedTo` | `number` | Filter by the **user id** of the assigned agent |\n| `search` | `string` | Case-insensitive match on customer name, email, subject or message text (max 200 characters) |\n| `page` | `number` | Page number (default 1) |\n| `limit` | `number` | Results per page (default 20, maximum 100) |\n\n**Response**\n\n```json\n{\n "data": [\n {\n "id": 101,\n "status": "ACTIVE",\n "channel": "WHATSAPP",\n "subject": null,\n "customer": { "id": 5, "name": "Chidi Okafor", "email": "chidi@palmex.com", "sessionId": "s_8f2c" },\n "assignedAgent": { "id": 7, "userId": 12, "user": { "fname": "Amara", "lname": "Osei", "email": "amara@kerabie.com", "avatar": null } },\n "lastMessage": "Please hurry.",\n "lastMessageAt": "2026-10-05T10:52:00.000Z",\n "tags": [{ "tag": "refund" }, { "tag": "vip" }],\n "aiHandling": false,\n "_count": { "messages": 1 },\n "createdAt": "2026-10-05T09:10:00.000Z",\n "updatedAt": "2026-10-05T10:52:00.000Z"\n }\n ],\n "meta": {\n "total": 43,\n "page": 1,\n "limit": 20,\n "pages": 3,\n "counts": { "all": 57, "WAITING": 4, "ACTIVE": 21, "RESOLVED": 12, "CLOSED": 20 }\n }\n}\n```\n\n**Field notes**\n\n- `tags` holds the first three tags only; [get a conversation](#get-a-conversation) returns all of them.\n- `_count.messages` is the number of **unread** messages.\n- `aiHandling` is `true` while the AI assistant is handling the conversation on its own: it is still open, the AI has replied, no human has, and it has not handed off.\n- `meta.counts` are organisation-wide totals per status and **ignore** the filters above, so they work as inbox tab counts.\n\n---\n\n## Get a conversation\n\n```http\nGET /v1/conversations/:id\n```\n\n```typescript\nconst conversation = await kerabie.conversations.get(101);\n```\n\nReturns the conversation with its customer, assigned agent, **all** tags and the `aiHandling` flag. Responds `404` if the conversation does not exist in your organisation.\n\n---\n\n## Previous conversations\n\n```http\nGET /v1/conversations/:id/history\n```\n\n```typescript\nconst { data } = await kerabie.conversations.getHistory(101, 5);\n```\n\nThe same customer\'s **other** conversations in your organisation, newest first. `limit` defaults to 5 and is capped at 20. The conversation you ask about is never included. A conversation with no linked customer returns an empty list.\n\n```json\n{\n "data": [\n {\n "id": 88,\n "status": "RESOLVED",\n "channel": "WHATSAPP",\n "subject": "Refund not received",\n "isTicket": false,\n "createdAt": "2026-09-28T09:00:00.000Z",\n "lastMessageAt": "2026-09-28T09:40:00.000Z",\n "handoffSummary": null\n }\n ]\n}\n```\n\n---\n\n## Take over from the AI\n\n```http\nPOST /v1/conversations/:id/take-over\n```\n\n```typescript\nawait kerabie.conversations.takeOver(101, { agentId: 7 });\n```\n\nHands a conversation from the AI assistant to a person. It **stops the AI** from replying, sets the status to `ACTIVE`, and assigns the conversation to `agentId` if nobody holds it yet (an existing assignee is kept).\n\n**Body**\n\n| Field | Type | Description |\n|---|---|---|\n| `agentId` | `number` | **Required.** The organisation team-member id to assign. |\n\n**Errors**\n\n| Status | When |\n|---|---|\n| `400` | `agentId` is missing or invalid, the team member is not in your organisation, or the AI is no longer handling the conversation (for example, someone already took over or it was resolved). |\n| `404` | The conversation does not exist in your organisation. |\n\nTaking over is safe to race: only one caller succeeds, and the rest receive `400`.\n\n---\n\n## Resolve a conversation\n\n```http\nPOST /conversations/:id/resolve\n```\n\n---\n\n## Close a conversation\n\n```http\nPOST /conversations/:id/close\n```\n\n---\n\n## Assign to an agent\n\n```http\nPATCH /conversations/:id\n```\n\n```json\n{ "agentId": 7 }\n```\n\n---\n\n## Send a message\n\n```http\nPOST /conversations/:id/messages\n```\n\n```json\n{\n "body": "Your order has shipped! Tracking: ABC123",\n "senderType": "bot"\n}\n```\n\n`senderType` can be `"agent"` or `"bot"`.', headings: [{ level: 2, text: "List conversations" }, { level: 2, text: "Get a conversation" }, { level: 2, text: "Previous conversations" }, { level: 2, text: "Take over from the AI" }, { level: 2, text: "Resolve a conversation" }, { level: 2, text: "Close a conversation" }, { level: 2, text: "Assign to an agent" }, { level: 2, text: "Send a message" }] }, { id: "api-reference/customers", url: "https://kerabie.com/docs/api-reference/customers", title: "Customers", description: "Look up and manage customers (visitors who have started conversations).", section: "api-reference", body: '## List customers\n\n```http\nGET /customers\n```\n\n**Query params:** `page`, `limit`, `search` (email or name), `from`, `to`\n\n---\n\n## Get a customer\n\n```http\nGET /customers/:id\n```\n\n---\n\n## Get by email\n\n```http\nGET /customers/by-email/:email\n```\n\n---\n\n## Update a customer\n\n```http\nPATCH /customers/:id\n```\n\n```json\n{\n "name": "Jane Smith",\n "email": "jane@example.com",\n "attributes": {\n "plan": "pro",\n "company": "Acme Corp",\n "userId": "user_123"\n }\n}\n```\n\n---\n\n## Delete a customer (GDPR)\n\n```http\nDELETE /customers/:id\n```\n\nPermanently removes all PII associated with this customer. Conversation content is anonymised but preserved for analytics. This action is irreversible.', headings: [{ level: 2, text: "List customers" }, { level: 2, text: "Get a customer" }, { level: 2, text: "Get by email" }, { level: 2, text: "Update a customer" }, { level: 2, text: "Delete a customer (GDPR)" }] }, { id: "api-reference/growth", url: "https://kerabie.com/docs/api-reference/growth", title: "Growth & Partner API", description: "UTM attribution tracking, traffic analytics, and partner/affiliate management.", section: "api-reference", body: 'The Growth API covers two systems:\n1. **UTM attribution** \u2014 track where visitors came from (Google Ads, Instagram, partner blogs, etc.)\n2. **Partner program** \u2014 manage affiliate partners, referrals, and commissions\n\n## UTM Attribution\n\n### Record a visit\n\nCall this on page load to record a UTM-tagged visit. The frontend SDK does this automatically via `useUtmCapture`.\n\n```http\nPOST /growth/utm/visit\nContent-Type: application/json\n```\n\n```json\n{\n "sessionId": "ks_abc123",\n "utmSource": "google",\n "utmMedium": "cpc",\n "utmCampaign": "summer_launch",\n "referralCode": "EMMAB2C",\n "landingPage": "https://kerabie.com/?utm_source=google&utm_medium=cpc"\n}\n```\n\nWhen a user signs up, pass the same UTM fields in the registration payload. The backend links the visit and credits the partner automatically.\n\n### UTM analytics summary\n\n```http\nGET /growth/utm/analytics?days=30\nAuthorization: Bearer YOUR_API_KEY\n```\n\n```json\n{\n "period": "30d",\n "visits": 1240,\n "conversions": 87,\n "conversionRate": 7.0,\n "topSources": [\n { "source": "google", "count": 542 },\n { "source": "instagram", "count": 311 }\n ],\n "topCampaigns": [\n { "campaign": "summer_launch", "count": 398 }\n ]\n}\n```\n\n### UTM timeline\n\n```http\nGET /growth/utm/timeline?days=30\nAuthorization: Bearer YOUR_API_KEY\n```\n\nReturns daily visit and conversion counts for charting.\n\n---\n\n## Partner Program\n\n### Apply as a partner\n\n```http\nPOST /partner/apply\nAuthorization: Bearer YOUR_API_KEY\n```\n\n```json\n{\n "displayName": "TechBlog Nigeria",\n "website": "https://techblog.ng",\n "description": "I write about SaaS tools for African businesses",\n "referralCode": "TECHBLOG"\n}\n```\n\nPartners start with `pending_review` status. Once approved, they get a referral link:\n`https://kerabie.com/?ref=TECHBLOG`\n\n### Partner dashboard\n\n```http\nGET /partner/me\nAuthorization: Bearer YOUR_API_KEY\n```\n\nReturns the partner profile, recent referrals, earnings, and payouts.\n\n### Earnings breakdown\n\n```http\nGET /partner/earnings?days=30\nAuthorization: Bearer YOUR_API_KEY\n```\n\n```json\n{\n "summary": {\n "totalEarned": 25000,\n "totalPaid": 10000,\n "pendingBalance": 15000,\n "totalClicks": 342,\n "totalSignups": 18\n },\n "earnings": [...],\n "timeline": [\n { "date": "2026-06-01", "earned": 5000, "pending": 5000, "paid": 0 }\n ]\n}\n```\n\n### Verify a referral code\n\n```http\nGET /partner/verify/{code}\n```\n\n```json\n{\n "valid": true,\n "partner": {\n "referralCode": "TECHBLOG",\n "displayName": "TechBlog Nigeria"\n }\n}\n```\n\n### Request payout\n\n```http\nPOST /partner/payout/request\nAuthorization: Bearer YOUR_API_KEY\n```\n\n```json\n{ "amount": 10000 }\n```\n\nPayouts are processed within 2\u20133 business days to the partner\'s registered bank account.\n\n---\n\n## How UTM flows to Partner commission\n\n1. Visitor lands on `kerabie.com/?ref=TECHBLOG`\n2. Frontend calls `POST /growth/utm/visit` with `referralCode: "TECHBLOG"`\n3. Partner\'s `totalClicks` increments\n4. Visitor signs up \u2014 registration payload includes `referralCode`, `utmSessionId`\n5. Backend links the `UtmVisit` \u2192 creates `PartnerReferral` \u2192 increments `totalSignups`\n6. When visitor subscribes to a paid plan \u2192 `PartnerEarning` is created (20% commission)', headings: [{ level: 2, text: "UTM Attribution" }, { level: 3, text: "Record a visit" }, { level: 3, text: "UTM analytics summary" }, { level: 3, text: "UTM timeline" }, { level: 2, text: "Partner Program" }, { level: 3, text: "Apply as a partner" }, { level: 3, text: "Partner dashboard" }, { level: 3, text: "Earnings breakdown" }, { level: 3, text: "Verify a referral code" }, { level: 3, text: "Request payout" }, { level: 2, text: "How UTM flows to Partner commission" }] }, { id: "api-reference/knowledge-base", url: "https://kerabie.com/docs/api-reference/knowledge-base", title: "Knowledge Base", description: "Manage articles via the authenticated API, and read published articles from the public widget-facing endpoints.", section: "api-reference", body: 'Kerabie exposes two separate knowledge base surfaces:\n\n- **`/v1/kb/*`** \u2014 authenticated CRUD for your own articles, using an API key (see [Authentication](/docs/api-reference/authentication)). Available on every plan.\n- **`/public/kb/*`** \u2014 read-only, published-only articles for embeddable consumers (the chat widget, the React Native helpdesk package). No API key \u2014 scoped by the widget key your embed already uses.\n\nEvery published article is also fed automatically into the AI chatbot as answer context, so anything you publish here can be used to answer visitor questions without extra setup.\n\n## Authenticated API (`/v1/kb`)\n\n### List articles\n\n```http\nGET /v1/kb/articles\nAuthorization: Bearer ker_live_abc123...\n```\n\n**Query params**\n\n| Param | Type | Description |\n|---|---|---|\n| `search` | `string` | Full-text search across title and content |\n| `category` | `string` | Filter by category |\n| `page` | `number` | Page number (default: 1) |\n| `limit` | `number` | Results per page (default 20) |\n\n**Response**\n\n```json\n{\n "data": [\n {\n "id": 12,\n "title": "How to reset your password",\n "content": "<p>Go to the login page and click Forgot Password...</p>",\n "type": "faq",\n "category": "Account",\n "status": "published",\n "imageUrl": "https://cdn.kerabie.com/knowledge-base/42/12-1234.jpg",\n "views": 245,\n "helpfulPct": 92,\n "votes": 30,\n "aiCitations": 88,\n "createdAt": "2026-03-15T10:00:00Z",\n "updatedAt": "2026-03-15T10:00:00Z"\n }\n ],\n "meta": { "total": 43, "page": 1, "limit": 20, "hasMore": true }\n}\n```\n\n`content` is HTML: the dashboard editor, the chat widget and the apps all store and show it that way. Plain text sent through the API or a CSV import is shown as-is, so send HTML (`<p>\u2026</p>`) for paragraphs and formatting.\n\n`helpfulPct` is the share of widget visitors who answered "Was this helpful?" with yes, and stays `null` until at least 3 have voted. `aiCitations` counts the AI chatbot answers that cited the article.\n\n---\n\n### Stats\n\n```http\nGET /v1/kb/stats\n```\n\n```json\n{ "total": 43, "published": 38, "drafts": 5, "views": 12840, "top": { "title": "How to reset your password", "views": 1240 }, "categories": ["Account", "Billing"] }\n```\n\n### Insights: what to write or fix next\n\n```http\nGET /v1/kb/insights\n```\n\n```json\n{\n "mostViewed": [{ "id": 12, "title": "How to reset your password", "views": 1240, "pct": 100 }],\n "gaps": [{ "question": "okta sso", "count": 23 }],\n "lowestRated": [{ "id": 7, "title": "Do you offer refunds?", "helpfulPct": 74, "votes": 12 }],\n "stale": [{ "id": 9, "title": "Connect WhatsApp", "age": "4 months" }]\n}\n```\n\n`gaps` are customer questions in the last 30 days that the AI had no article to answer from. `lowestRated` needs at least 3 votes per article. `stale` lists published articles not updated in 90 days. Cached for a few minutes.\n\n---\n\n### Get an article\n\n```http\nGET /v1/kb/articles/:id\n```\n\n---\n\n### Create an article\n\n```http\nPOST /v1/kb/articles\n```\n\n```json\n{\n "title": "How to reset your password",\n "content": "<p>Go to the login page...</p>",\n "category": "Account",\n "type": "faq",\n "status": "published"\n}\n```\n\n`type` is one of `faq | article | news | information` (default `article`). `status` is one of `draft | published | archived` (default `published`).\n\n---\n\n### Update an article\n\n```http\nPATCH /v1/kb/articles/:id\n```\n\nAccepts the same fields as create; only send what\'s changing.\n\n---\n\n### Delete an article\n\n```http\nDELETE /v1/kb/articles/:id\n```\n\n---\n\n## Public, widget-facing API (`/public/kb`)\n\nNo API key. Authenticate with your widget key, either as an `x-widget-key` header or a `widgetKey` query param \u2014 the same key your chat widget embed already uses.\n\n### List published articles\n\n```http\nGET /public/kb/articles?widgetKey=wk_yourorg_abc123&search=refund\n```\n\n**Query params**\n\n| Param | Type | Description |\n|---|---|---|\n| `search` | `string` | Full-text search |\n| `category` | `string` | Filter by category |\n| `type` | `string` | Filter by `faq \\| article \\| news \\| information` |\n| `page` / `limit` | `number` | Pagination |\n\nReturns only `status: "published"` articles, the same `{ data, meta }` shape as the authenticated list.\n\n### Get a published article\n\n```http\nGET /public/kb/articles/:id?widgetKey=wk_yourorg_abc123\n```\n\nIncrements the article\'s view count.\n\n### Rate an article\n\n```http\nPOST /public/kb/articles/:id/feedback?widgetKey=wk_yourorg_abc123\n{ "helpful": true }\n```\n\nThe "Was this helpful?" vote from the widget and the React Native package. A visitor can vote up to 3 times per article per day (`429` after that); `404` for a draft or unknown article. Votes feed the helpfulness shown in the dashboard.\n\n The chat widget\'s built-in search screen calls a simpler variant of this, `GET /public/knowledge-base?widgetKey=...&q=...`, returning `{ items: [...] }` \u2014 that\'s what actually powers the widget UI. The `/public/kb/*` endpoints above exist for consumers that need pagination or a single-article lookup, like the React Native helpdesk package.\n\n## Importing content\n\n Import, crawl, and refine are dashboard actions, authenticated with your logged-in session (not an API key) \u2014 they live under `/knowledge-base/*`, not `/v1/kb/*`. They\'re documented here because they\'re part of the same feature, not because they\'re callable with a `ker_live_` key.\n\nThree ways to get content into the knowledge base beyond typing it manually \u2014 all AI-assisted and gated by plan (see below).\n\n### Upload a document\n\n```http\nPOST /knowledge-base/import/document\nContent-Type: multipart/form-data\n```\n\nUpload a file (max 5MB). Everything becomes drafts for you to review.\n\n- **`.csv`**: one article per row, no AI step and no AI quota used. Use a header row with `title` (or `question`) and `content` (or `answer`); `category` and `type` are optional. Up to 500 rows; rows that can\'t be used are returned in `skipped` with their row number.\n- **`.docx`, `.md`, `.txt`, `.pdf`, `.json`**: AI structures the text into one or more draft articles.\n\n### Crawl a website\n\nA two-step flow \u2014 you pick which pages get imported before anything runs:\n\n```http\nPOST /knowledge-base/crawl/discover\n{ "url": "https://docs.yoursite.com" }\n```\n\nReturns candidate pages found on that domain (from on-page links and, when present, `sitemap.xml`):\n\n```json\n{ "pages": [{ "url": "https://docs.yoursite.com/faq", "title": "FAQ" }] }\n```\n\n```http\nPOST /knowledge-base/crawl/import\n{ "urls": ["https://docs.yoursite.com/faq"] }\n```\n\nRuns as a background job (crawling and AI structuring can take a while for several pages) and responds immediately with a job ID:\n\n```json\n{ "jobId": "42" }\n```\n\nPoll for completion:\n\n```http\nGET /knowledge-base/jobs/:jobId\n```\n\n```json\n{ "status": "succeeded", "result": { "imported": 4, "items": [...] } }\n```\n\n### Refine an article with AI\n\n```http\nPOST /knowledge-base/:id/refine\n```\n\nStreams the refined content back as Server-Sent Events (`data: { "delta": "..." }` chunks, ending with `data: { "done": true }`) and saves the result once the stream completes. AI-generated text never uses em dashes.\n\n## Plan limits\n\nImport, crawl, and refine share one monthly AI-operations budget per organisation, plus crawl has its own per-run page cap:\n\n| Plan | AI ops / month | Pages per crawl |\n|---|---|---|\n| Free | Not available | Not available |\n| Basic | 10 | 20 |\n| Pro | 50 | 100 |\n| Enterprise | Unlimited | Unlimited |\n\nExceeding either limit returns a `403` with a message naming the limit.', headings: [{ level: 2, text: "Authenticated API (`/v1/kb`)" }, { level: 3, text: "List articles" }, { level: 3, text: "Stats" }, { level: 3, text: "Insights: what to write or fix next" }, { level: 3, text: "Get an article" }, { level: 3, text: "Create an article" }, { level: 3, text: "Update an article" }, { level: 3, text: "Delete an article" }, { level: 2, text: "Public, widget-facing API (`/public/kb`)" }, { level: 3, text: "List published articles" }, { level: 3, text: "Get a published article" }, { level: 3, text: "Rate an article" }, { level: 2, text: "Importing content" }, { level: 3, text: "Upload a document" }, { level: 3, text: "Crawl a website" }, { level: 3, text: "Refine an article with AI" }, { level: 2, text: "Plan limits" }] }, { id: "api-reference/overview", url: "https://kerabie.com/docs/api-reference/overview", title: "API Overview", description: "REST API for the Kerabie customer support platform.", section: "api-reference", body: 'The Kerabie REST API lets you build custom integrations, automate workflows, and access your support data programmatically.\n\n## Base URL\n\n```\nhttps://api.kerabie.com\n```\n\n## Authentication\n\nAll API requests require a Bearer token and your org ID:\n\n```http\nAuthorization: Bearer YOUR_API_KEY\nX-Kerabie-Org: YOUR_ORG_ID\nContent-Type: application/json\n```\n\nGenerate an API key in **Settings \u2192 API \u2192 New API Key**. Keys are scoped to your organisation.\n\n## Interactive docs\n\nTry every endpoint directly in the browser \u2014 no separate tool needed:\n\n**[Open API Playground \u2192](/docs/api-reference/playground)**\n\nThe playground is powered by Swagger UI and includes every endpoint with request/response schemas. Authorize with your API key once and test live requests against the real API.\n\n## Rate limits\n\n| Plan | Requests per minute |\n|---|---|\n| Free | 30 |\n| Starter | 120 |\n| Pro | 600 |\n| Business | 3,000 |\n\nRate limit headers are returned on every response:\n\n```http\nX-RateLimit-Limit: 120\nX-RateLimit-Remaining: 87\nX-RateLimit-Reset: 1716734040\n```\n\n## Errors\n\nAll errors follow a consistent format:\n\n```json\n{\n "statusCode": 404,\n "message": "Conversation not found",\n "error": "Not Found"\n}\n```\n\n| Status | Meaning |\n|---|---|\n| `400` | Bad request \u2014 missing or invalid parameters |\n| `401` | Unauthorised \u2014 missing or invalid API key |\n| `403` | Forbidden \u2014 you don\'t have access to this resource |\n| `404` | Not found |\n| `429` | Rate limit exceeded |\n| `500` | Server error |\n\n## Pagination\n\nList endpoints return paginated results:\n\n```json\n{\n "data": [...],\n "total": 243,\n "page": 1,\n "limit": 20\n}\n```\n\nPass `?page=2&limit=50` to navigate pages.\n\n## Use with an AI assistant\n\nConnect the [Kerabie MCP server](/docs/sdks/mcp) to Claude, Cursor or another MCP client and it can search these docs, read the API and TypeScript SDK types and follow the integration guide while it writes code for you. No API key is needed for that.', headings: [{ level: 2, text: "Base URL" }, { level: 2, text: "Authentication" }, { level: 2, text: "Interactive docs" }, { level: 2, text: "Rate limits" }, { level: 2, text: "Errors" }, { level: 2, text: "Pagination" }, { level: 2, text: "Use with an AI assistant" }] }, { id: "api-reference/store", url: "https://kerabie.com/docs/api-reference/store", title: "Store & Products API", description: "Manage your Kerabie store, products, orders, and product analytics.", section: "api-reference", body: 'The Store API lets you manage products, process orders, and track per-product analytics (views, inquiries, sales).\n\n## Products\n\n### List products\n\n```http\nGET /store/products\nAuthorization: Bearer YOUR_API_KEY\n```\n\n```json\n{\n "products": [\n {\n "id": 1,\n "name": "Blue Sneakers",\n "price": 15000,\n "currency": "NGN",\n "imageUrl": "https://...",\n "category": "Footwear",\n "stock": 24,\n "views": 312,\n "inquiries": 18,\n "salesCount": 9,\n "salesAmount": 135000\n }\n ]\n}\n```\n\n### Create a product\n\n```http\nPOST /store/products\nAuthorization: Bearer YOUR_API_KEY\n```\n\n```json\n{\n "name": "Blue Sneakers",\n "description": "Comfortable everyday sneakers",\n "price": 15000,\n "currency": "NGN",\n "category": "Footwear",\n "imageUrl": "https://...",\n "stock": 50,\n "sku": "SNKR-BLU-001"\n}\n```\n\n### Product analytics (per-product stats)\n\n```http\nGET /store/products/stats\nAuthorization: Bearer YOUR_API_KEY\n```\n\nReturns products sorted by views (most viewed first), with view count, inquiry count, sales count, and revenue.\n\n```json\n{\n "products": [\n {\n "id": 1,\n "name": "Blue Sneakers",\n "views": 312,\n "inquiries": 18,\n "salesCount": 9,\n "salesAmount": 135000\n }\n ]\n}\n```\n\n## Public Storefront\n\nThese endpoints are unauthenticated \u2014 called by your public storefront.\n\n### Track product view\n\nCalled automatically by the Kerabie storefront when a visitor adds a product to cart.\n\n```http\nPOST /public/store/{slug}/view/{productId}\nContent-Type: application/json\n```\n\n```json\n{ "sessionId": "ks_abc123" }\n```\n\n### Track product inquiry\n\nCall this when a visitor opens chat asking about a specific product.\n\n```http\nPOST /public/store/{slug}/inquiry/{productId}\n```\n\n## Orders\n\n### List orders\n\n```http\nGET /store/orders?status=pending\nAuthorization: Bearer YOUR_API_KEY\n```\n\n### Update order status\n\n```http\nPATCH /store/orders/{id}\nAuthorization: Bearer YOUR_API_KEY\n```\n\n```json\n{ "status": "fulfilled" }\n```\n\nStatus values: `pending` | `paid` | `fulfilled` | `cancelled`', headings: [{ level: 2, text: "Products" }, { level: 3, text: "List products" }, { level: 3, text: "Create a product" }, { level: 3, text: "Product analytics (per-product stats)" }, { level: 2, text: "Public Storefront" }, { level: 3, text: "Track product view" }, { level: 3, text: "Track product inquiry" }, { level: 2, text: "Orders" }, { level: 3, text: "List orders" }, { level: 3, text: "Update order status" }] }, { id: "api-reference/tickets", url: "https://kerabie.com/docs/api-reference/tickets", title: "Tickets API", description: "Overview numbers and CSV export for tickets.", section: "api-reference", body: 'Tickets are conversations created through a form, email or the API, so they share the conversation model. These endpoints use your [API key](/docs/api-reference/authentication).\n\n```typescript\n\nconst kerabie = new KerAbieClient({ apiKey: process.env.KERABIE_API_KEY });\n```\n\n## Overview numbers\n\n```http\nGET /v1/tickets/stats\n```\n\n```typescript\nconst stats = await kerabie.tickets.getStats();\n```\n\n```json\n{\n "open": 4,\n "unassigned": 2,\n "breachingSla": 1,\n "avgResolutionMinutes": 192,\n "resolutionDeltaPct": -18\n}\n```\n\n| Field | Meaning |\n|---|---|\n| `open` | Tickets that are new or active. |\n| `unassigned` | Open tickets nobody owns. |\n| `breachingSla` | Open tickets past their SLA due time. |\n| `avgResolutionMinutes` | Average time from creation to resolution for tickets resolved or closed in the last 7 days. `null` when none were. |\n| `resolutionDeltaPct` | % change versus the 7 days before. **Negative is faster.** `null` when there is nothing to compare against. |\n\nResults are cached for up to a minute and refresh immediately when a ticket\'s status, priority or assignee changes.\n\n---\n\n## Export to CSV\n\n```http\nGET /v1/tickets/export\n```\n\n```typescript\nconst { filename, csv, count, truncated } = await kerabie.tickets.export({ status: \'ACTIVE\' });\n```\n\n| Query param | Description |\n|---|---|\n| `status` | `WAITING`, `ACTIVE`, `RESOLVED` or `CLOSED`. |\n| `priority` | `LOW`, `NORMAL`, `HIGH` or `URGENT`. |\n| `search` | Case-insensitive match on requester name or email, or subject. A ticket reference such as `KB-2041` (or just `2041`) matches by id. |\n| `assignedTo` | User id of the assigned agent. |\n\n```json\n{\n "filename": "tickets-2026-10-05.csv",\n "csv": "Ticket,Subject,Status,Priority,Channel,Requester,Requester email,Assignee,Created,Last activity,SLA due,SLA breached\r\nKB-2041,Refund not received,ACTIVE,URGENT,WHATSAPP,Chidi Okafor,chidi@palmex.com,Amara Osei,2026-10-05T09:10:00.000Z,2026-10-05T10:52:00.000Z,2026-10-05T11:00:00.000Z,yes\r\n",\n "count": 1,\n "truncated": false\n}\n```\n\n- At most **5,000** tickets are returned. If more matched, `truncated` is `true` \u2014 narrow the filters and export again.\n- `SLA breached` is `yes` or `no` for open tickets with an SLA, and empty otherwise.\n- **Plan requirement:** exporting needs a plan with data exports (Pro and above). Other plans receive `403 Forbidden`.\n- Cells beginning with `=`, `+`, `-` or `@` are prefixed with a single quote so spreadsheet apps show them as text rather than evaluating them as formulas.', headings: [{ level: 2, text: "Overview numbers" }, { level: 2, text: "Export to CSV" }] }, { id: "api-reference/visitors", url: "https://kerabie.com/docs/api-reference/visitors", title: "Visitors API", description: "Who is on your site, who has been, and who is blocked or allowed.", section: "api-reference", body: 'These endpoints use your [API key](/docs/api-reference/authentication) and return the same data as the **Visitors** page in the dashboard.\n\n```typescript\n\nconst kerabie = new KerAbieClient({ apiKey: process.env.KERABIE_API_KEY });\n```\n\nVisitor history is recorded when someone opens your chat widget and is kept for **30 days**. A visitor who never sends a message is still recorded \u2014 they just have no contact or conversation.\n\n## List visitors\n\n```http\nGET /v1/visitors\n```\n\n```typescript\nconst { data, meta } = await kerabie.visitors.list({ status: \'online\' });\n```\n\n| Query param | Description |\n|---|---|\n| `status` | `all` (default), `online`, `offline`, `blocked` or `allowed`. |\n| `search` | Case-insensitive match on name, email, IP, city or page. |\n| `country` | Two-letter country code, e.g. `NG`. |\n| `days` | How far back to look. Default `7`, maximum `30`. |\n| `page`, `limit` | Pagination. `limit` defaults to 20, maximum 100. |\n\n```json\n{\n "data": [\n {\n "sessionId": "sess_8f2c1d",\n "name": "Chidi Okafor",\n "email": "chidi@palmex.com",\n "phone": "+234 803 555 0142",\n "country": "NG",\n "city": "Lagos",\n "ip": "102.89.34.12",\n "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \u2026",\n "currentPage": "/pricing",\n "visitCount": 14,\n "firstSeenAt": "2026-09-28T09:10:00.000Z",\n "lastSeenAt": "2026-10-05T10:52:00.000Z",\n "conversationId": 101,\n "status": "online"\n }\n ],\n "meta": { "total": 43, "page": 1, "limit": 20, "pages": 3, "truncated": false }\n}\n```\n\n**Field notes**\n\n- **`status`** is derived from your IP rules and **wins over presence**: a visitor on the allowlist reads `allowed` even while online, and a blocked IP reads `blocked`. A blocklist match wins over an allowlist match, as it does when the visitor connects.\n- **`status=blocked`** and **`status=allowed`** match IP *ranges* (for example `203.0.113.0/24`) as well as exact IPs. They are checked against the 1,000 most recent matching visitors, and `meta.truncated` is `true` when that limit was reached.\n- **`ip`** is `null` when your organisation hides visitor IPs in its privacy settings. Blocking and allowing still apply to the real address.\n- **`visitCount`** goes up by one when a visitor comes back after being away for 30 minutes or more. Reloading a page or coming back quickly does not count.\n- **`name` and `email`** come from the contact record if the visitor has chatted, otherwise from what they told the widget.\n- **`currentPage`** is the page they are on right now for online visitors, otherwise the last page they were on.\n\n---\n\n## Overview numbers\n\n```http\nGET /v1/visitors/overview\n```\n\n```typescript\nconst overview = await kerabie.visitors.getOverview();\n```\n\n```json\n{\n "online": 3,\n "countries": 9,\n "returning": 7,\n "blockedIps": 1,\n "topCountries": [{ "country": "NG", "count": 5 }, { "country": "GB", "count": 4 }]\n}\n```\n\n| Field | Meaning |\n|---|---|\n| `online` | Visitors on the site right now (always live). |\n| `countries` | Distinct countries among visitors in the last 24 hours. |\n| `returning` | Visitors in the last 24 hours who have visited more than once. |\n| `blockedIps` | Active block rules. |\n| `topCountries` | Up to five countries by visitor count in the last 24 hours, most first. |\n\nThe last-24-hours numbers are cached for up to 30 seconds.\n\n---\n\n## Export to CSV\n\n```http\nGET /v1/visitors/export\n```\n\n```typescript\nconst { filename, csv, count, truncated } = await kerabie.visitors.export({ status: \'blocked\' });\n```\n\nAccepts `status`, `search`, `country` and `days` as above.\n\n- At most **5,000** visitors are returned; `truncated` is `true` if more matched.\n- The IP column is empty when your organisation hides visitor IPs.\n- **Plan requirement:** exporting needs a plan with data exports (Pro and above). Other plans receive `403 Forbidden`.\n- Cells beginning with `=`, `+`, `-` or `@` are prefixed with a single quote so spreadsheet apps show them as text instead of evaluating them as formulas. This matters here because visitor names and pages are typed by the public.', headings: [{ level: 2, text: "List visitors" }, { level: 2, text: "Overview numbers" }, { level: 2, text: "Export to CSV" }] }, { id: "api-reference/webhooks", url: "https://kerabie.com/docs/api-reference/webhooks", title: "Webhooks API", description: "Manage webhook endpoints via the API.", section: "api-reference", body: '## List webhooks\n\n```http\nGET /webhooks\n```\n\n---\n\n## Create a webhook\n\n```http\nPOST /webhooks\n```\n\n```json\n{\n "url": "https://yourserver.com/webhooks/kerabie",\n "events": ["conversation.created", "message.sent.agent"],\n "description": "CRM sync"\n}\n```\n\n**Response** includes the generated `secret` \u2014 store it securely. It\'s shown only once.\n\n---\n\n## Update a webhook\n\n```http\nPATCH /webhooks/:id\n```\n\n---\n\n## Delete a webhook\n\n```http\nDELETE /webhooks/:id\n```\n\n---\n\n## List delivery logs\n\n```http\nGET /webhooks/:id/deliveries\n```\n\nReturns recent delivery attempts with status, response code, and error message.\n\n---\n\n## Retry a delivery\n\n```http\nPOST /webhooks/:id/deliveries/:deliveryId/retry\n```\n\nManually retries a failed delivery.\n\n---\n\nFor details on events, payloads, and signature verification see the [Webhooks guide](/integrations/webhooks).', headings: [{ level: 2, text: "List webhooks" }, { level: 2, text: "Create a webhook" }, { level: 2, text: "Update a webhook" }, { level: 2, text: "Delete a webhook" }, { level: 2, text: "List delivery logs" }, { level: 2, text: "Retry a delivery" }] }, { id: "automations/actions", url: "https://kerabie.com/docs/automations/actions", title: "Actions", description: "What an automation can do when triggered.", section: "automations", body: "Actions are the steps an automation performs after its trigger fires and conditions are met. An automation can have **multiple actions** \u2014 they run in sequence.\n\n## Assignment actions\n\n| Action | Description |\n|---|---|\n| `Assign agent` | Assigns the conversation to a specific agent |\n| `Assign team` | Assigns to a team using round-robin across available agents |\n| `Unassign` | Removes the current assignee |\n\n## Messaging actions\n\n| Action | Description |\n|---|---|\n| `Send message` | Sends an automated message to the visitor |\n| `Send internal note` | Posts a note visible only to agents (not the visitor) |\n\nWhen using `Send message`, you can use template variables:\n\n| Variable | Value |\n|---|---|\n| `{{visitor_name}}` | Visitor's name if known |\n| `{{agent_name}}` | Assigned agent's name |\n| `{{org_name}}` | Your organisation name |\n\n## Status actions\n\n| Action | Description |\n|---|---|\n| `Resolve conversation` | Marks the conversation resolved |\n| `Reopen conversation` | Reopens a resolved conversation |\n| `Add tag` | Adds a tag to the conversation |\n| `Remove tag` | Removes a specific tag |\n\n## Notification actions\n\n| Action | Description |\n|---|---|\n| `Notify agent` | Sends a push and/or email alert to a specific agent |\n| `Notify team` | Alerts all members of a team |\n\n## Action limits\n\n- Maximum **10 actions** per automation\n- `Send message` actions include a short delay (1\u20133 seconds) to avoid instant bot responses that feel robotic\n- Actions run in the order listed \u2014 drag to reorder\n\n## Example: full automation\n\n**Goal**: Route VIP customers to the senior support team and notify the manager.\n\n1. **Trigger**: Conversation created\n2. **Condition**: Visitor attribute `plan` = `enterprise`\n3. **Actions**:\n - Assign team \u2192 Senior Support\n - Add tag \u2192 `vip`\n - Notify agent \u2192 Manager (Alice)\n - Send message \u2192 \"Hi `{{visitor_name}}`, you're connected with our senior support team. We'll be right with you.\"", headings: [{ level: 2, text: "Assignment actions" }, { level: 2, text: "Messaging actions" }, { level: 2, text: "Status actions" }, { level: 2, text: "Notification actions" }, { level: 2, text: "Action limits" }, { level: 2, text: "Example: full automation" }] }, { id: "automations/flows", url: "https://kerabie.com/docs/automations/flows", title: "Message Flows", description: "Build branching conversation flows with decision trees.", section: "automations", body: 'Message Flows let you build interactive bot conversations with branching paths \u2014 like a decision tree. Visitors choose from options and the flow guides them to the right answer or agent.\n\n<Note>Message Flows are available on **Pro** and above.</Note>\n\n## How flows work\n\nA flow is a sequence of **steps**. Each step is one of:\n\n| Step type | Description |\n|---|---|\n| `Message` | Send a text message to the visitor |\n| `Question` | Ask a question with multiple-choice options |\n| `Condition` | Branch based on a visitor attribute or previous answer |\n| `Assign` | Assign the conversation to an agent or team |\n| `End` | Close the flow (visitor can still send messages) |\n\nWhen a visitor opens the widget, the flow starts automatically (if configured) or is triggered by an automation.\n\n## Creating a flow\n\n1. Go to **Settings \u2192 Automations \u2192 Flows \u2192 New flow**\n2. Give the flow a name\n3. Add steps using the visual canvas:\n - Drag steps from the left panel\n - Connect them by drawing lines between step outputs\n4. Set the **entry point** \u2014 the first step that runs\n5. Click **Activate** when ready\n\n## Example: booking triage flow\n\n```\n[Message] "Hi! What can I help you with today?"\n |\n[Question] \n \u2192 "Book a call" \u2192 [Assign] Sales team \u2192 [End]\n \u2192 "Technical issue" \u2192 [Assign] Support team \u2192 [End]\n \u2192 "Billing query" \u2192 [Message] "Please email billing@example.com" \u2192 [End]\n```\n\n## Using variables in flow messages\n\nUse the same template variables as automations:\n\n```\nHi {{visitor_name}}! I\'m here to help. What brings you here today?\n```\n\n## Triggering a flow\n\nFlows can be triggered:\n\n- **Automatically on widget open** \u2014 set in the flow settings as the default entry flow\n- **Via an automation action** \u2014 add a `Start flow` action to any automation\n\n## Fallback to live agent\n\nAlways include a path that connects to a live agent. Visitors who get stuck in a flow can feel abandoned \u2014 a clear "Talk to a human" option prevents drop-off.\n\n## Analytics\n\nTrack flow performance in **Analytics \u2192 Flows**:\n- Completion rate per step\n- Drop-off points\n- Which paths visitors take most', headings: [{ level: 2, text: "How flows work" }, { level: 2, text: "Creating a flow" }, { level: 2, text: "Example: booking triage flow" }, { level: 2, text: "Using variables in flow messages" }, { level: 2, text: "Triggering a flow" }, { level: 2, text: "Fallback to live agent" }, { level: 2, text: "Analytics" }] }, { id: "automations/overview", url: "https://kerabie.com/docs/automations/overview", title: "Automations", description: "Automatically route conversations, send messages, and trigger actions based on rules.", section: "automations", body: 'Automations let you define rules that run whenever a trigger event occurs. Use them to route conversations, greet visitors, assign agents, and more \u2014 without any manual work.\n\n## Anatomy of an automation\n\nEvery automation has three parts:\n\n- **Trigger** \u2014 what event starts the rule (e.g. "conversation created")\n- **Conditions** \u2014 optional filters (e.g. "page URL contains /pricing")\n- **Actions** \u2014 what to do (e.g. "assign to Sales team")\n\n## Common use cases\n\n| Goal | Trigger | Action |\n|---|---|---|\n| Route pricing questions to Sales | Conversation created + URL contains `/pricing` | Assign to Sales team |\n| Greet returning visitors | Visitor started + has previous conversations | Send message: "Welcome back!" |\n| Auto-close idle conversations | No activity for 24 hours | Resolve conversation |\n| Tag bug reports | Message contains "bug" or "broken" | Add tag: `bug-report` |\n| Notify on VIP customer | Conversation created + attribute `plan = enterprise` | Notify specific agent |\n\n## Creating an automation\n\n1. Go to **Settings \u2192 Automations \u2192 New automation**\n2. Give it a name\n3. Choose a **trigger** event\n4. Add **conditions** (optional \u2014 leave empty to match all)\n5. Add one or more **actions**\n6. Toggle it **Active** and save\n\n## Triggers\n\n| Trigger | When it fires |\n|---|---|\n| `Conversation created` | A new conversation starts |\n| `Message received` | A visitor sends a message |\n| `Visitor page view` | A visitor views a page *(Pro)* |\n| `Conversation resolved` | A conversation is marked resolved |\n| `No activity` | No messages for a set time |\n| `Visitor attribute` | A visitor attribute matches a value |\n\n## Actions\n\n| Action | What it does |\n|---|---|\n| `Send message` | Bot sends a message to the visitor |\n| `Assign agent` | Assigns the conversation to a specific agent |\n| `Assign team` | Assigns to a team (round-robin) |\n| `Add tag` | Tags the conversation |\n| `Resolve` | Marks conversation as resolved |\n| `Notify agent` | Sends a push/email alert to an agent |', headings: [{ level: 2, text: "Anatomy of an automation" }, { level: 2, text: "Common use cases" }, { level: 2, text: "Creating an automation" }, { level: 2, text: "Triggers" }, { level: 2, text: "Actions" }] }, { id: "automations/triggers", url: "https://kerabie.com/docs/automations/triggers", title: "Triggers", description: "Events that start an automation rule.", section: "automations", body: "A trigger is the event that causes an automation to fire. Every automation has exactly one trigger.\n\n## Available triggers\n\n### Conversation triggers\n\n| Trigger | When it fires |\n|---|---|\n| `Conversation created` | A new conversation starts (visitor sends first message) |\n| `Conversation resolved` | A conversation is marked resolved |\n| `Conversation reopened` | A resolved conversation receives a new message |\n| `No activity` | No messages received for a configured duration |\n\n### Message triggers\n\n| Trigger | When it fires |\n|---|---|\n| `Message received` | A visitor sends any message |\n| `Message contains` | A visitor message matches a keyword or phrase |\n\n### Visitor triggers\n\n| Trigger | When it fires |\n|---|---|\n| `Visitor started session` | A visitor opens the widget for the first time in a session |\n| `Visitor page view` *(Pro)* | A visitor views a specific page |\n| `Visitor attribute` | A visitor attribute matches a value (e.g. `plan = enterprise`) |\n\n## Combining trigger with conditions\n\nA trigger fires the rule \u2014 **conditions** then narrow down whether the actions run. For example:\n\n- **Trigger**: `Conversation created`\n- **Condition**: Page URL contains `/pricing`\n- **Action**: Assign to Sales team\n\nWithout the condition, every new conversation would be assigned to Sales. The condition restricts it to conversations started on the pricing page.\n\nSee [Actions](/docs/automations/actions) for what an automation can do once triggered.\n\n## Trigger order\n\nIf multiple automations share the same trigger and their conditions all match, they run **in the order they appear** in the automation list. Drag automations to reorder them.\n\n## Testing a trigger\n\nUse the **Test** button when editing an automation. It runs the automation against the most recent matching conversation so you can verify conditions and actions work as expected before going live.", headings: [{ level: 2, text: "Available triggers" }, { level: 3, text: "Conversation triggers" }, { level: 3, text: "Message triggers" }, { level: 3, text: "Visitor triggers" }, { level: 2, text: "Combining trigger with conditions" }, { level: 2, text: "Trigger order" }, { level: 2, text: "Testing a trigger" }] }, { id: "billing/invoices", url: "https://kerabie.com/docs/billing/invoices", title: "Invoices & Receipts", description: "Download and manage your billing history.", section: "billing", body: "## Download invoices\n\nAll invoices are available in **Settings \u2192 Billing \u2192 Invoices**. Each invoice is a PDF you can download and share with your finance team.\n\n## Invoice details\n\nEach invoice includes:\n- Invoice number and date\n- Your organisation name and address\n- Itemised charges (plan, seat count, billing period)\n- Payment method (last 4 digits)\n- Amount paid\n\n## Adding a billing email\n\nBy default invoices are sent to the org owner's email. To send them to a separate billing address (e.g. `finance@yourcompany.com`):\n\nGo to **Settings \u2192 Billing \u2192 Billing email** and enter the address.\n\n## Tax / VAT\n\nTo add a tax ID or company details to your invoices, go to **Settings \u2192 Billing \u2192 Tax information**.\n\n## Failed payments\n\nIf a payment fails, Kerabie retries automatically over several days:\n\n| Attempt | Timing |\n|---|---|\n| Initial charge | Billing date |\n| 1st retry | +2 days |\n| 2nd retry | +4 days |\n| 3rd retry | +7 days |\n\nAfter 3 failures, your subscription is downgraded to Free and you'll receive an email.\n\nTo fix a payment failure, go to **Settings \u2192 Billing \u2192 Update payment method**.", headings: [{ level: 2, text: "Download invoices" }, { level: 2, text: "Invoice details" }, { level: 2, text: "Adding a billing email" }, { level: 2, text: "Tax / VAT" }, { level: 2, text: "Failed payments" }] }, { id: "billing/plans", url: "https://kerabie.com/docs/billing/plans", title: "Plans & Pricing", description: "Compare Kerabie plans and understand what's included.", section: "billing", body: `## Plan comparison
|
|
441
|
-
|
|
442
|
-
| Feature | Free | Starter | Pro | Business |
|
|
443
|
-
|---|---|---|---|---|
|
|
444
|
-
| Agents | 1 | 3 | 10 | Unlimited |
|
|
445
|
-
| Conversations/month | Unlimited | Unlimited | Unlimited | Unlimited |
|
|
446
|
-
| AI chats/month | \u2014 | 200 | 2,000 | 10,000 |
|
|
447
|
-
| Chat widgets | 1 | 2 | 5 | Unlimited |
|
|
448
|
-
| Visitor tracking | \u2014 | \u2014 | \u2713 | \u2713 |
|
|
449
|
-
| Knowledge base articles | 10 | 50 | Unlimited | Unlimited |
|
|
450
|
-
| Remove Kerabie branding | \u2014 | \u2014 | \u2713 | \u2713 |
|
|
451
|
-
| Virtual assistants | \u2014 | \u2713 | \u2713 | \u2713 |
|
|
452
|
-
| Custom automations | \u2014 | 5 | Unlimited | Unlimited |
|
|
453
|
-
| Campaigns | \u2014 | \u2014 | \u2713 | \u2713 |
|
|
454
|
-
| Analytics export | \u2014 | \u2014 | \u2713 | \u2713 |
|
|
455
|
-
| Priority support | \u2014 | \u2014 | \u2014 | \u2713 |
|
|
456
|
-
| SLA | \u2014 | \u2014 | \u2014 | \u2713 |
|
|
457
|
-
|
|
458
|
-
All paid plans include a **14-day free trial**. No credit card required to start.
|
|
459
|
-
|
|
460
|
-
## Billing cycle
|
|
461
|
-
|
|
462
|
-
Plans are billed monthly or yearly. Yearly plans get 2 months free.
|
|
463
|
-
|
|
464
|
-
## What counts as an "AI chat"
|
|
465
|
-
|
|
466
|
-
Each time the AI sends a reply to a visitor counts as one AI chat. If the AI escalates to a human agent, subsequent messages from the agent don't count. One conversation may involve multiple AI chats if the visitor sends several questions.
|
|
467
|
-
|
|
468
|
-
## Overage
|
|
469
|
-
|
|
470
|
-
If you exceed your AI chat limit, the AI stops responding for the rest of the month and conversations go straight to human agents. Upgrade your plan to restore AI \u2014 no automatic overage charges.`, headings: [{ level: 2, text: "Plan comparison" }, { level: 2, text: "Billing cycle" }, { level: 2, text: 'What counts as an "AI chat"' }, { level: 2, text: "Overage" }] }, { id: "billing/upgrading", url: "https://kerabie.com/docs/billing/upgrading", title: "Upgrading & Downgrading", description: "How to change your plan.", section: "billing", body: "## Upgrade\n\n1. Go to **Settings \u2192 Billing \u2192 Plans**\n2. Click **Upgrade** on the plan you want\n3. Enter payment details (card via Paystack)\n4. Confirm \u2014 your new plan is active immediately\n\nWhen you upgrade mid-cycle, you're charged a pro-rated amount for the remaining days.\n\n## Downgrade\n\n1. Go to **Settings \u2192 Billing \u2192 Plans**\n2. Click **Downgrade**\n3. Confirm\n\nDowngrades take effect at the end of your current billing period. You keep your current plan's features until then.\n\n Downgrading may reduce your agent seats, AI chat quota, or widget count. Features above the new plan's limit will be deactivated at the end of the billing period.\n\n## Cancel\n\nGo to **Settings \u2192 Billing \u2192 Cancel subscription**. Your plan continues until the end of the billing period, then switches to Free.\n\nYour data is preserved on the Free plan \u2014 you don't lose conversation history when you cancel.\n\n## Reactivate\n\nIf you cancelled and want to reactivate, just go to **Settings \u2192 Billing \u2192 Plans** and choose a plan. Your previous data is intact.", headings: [{ level: 2, text: "Upgrade" }, { level: 2, text: "Downgrade" }, { level: 2, text: "Cancel" }, { level: 2, text: "Reactivate" }] }, { id: "campaigns/announcements", url: "https://kerabie.com/docs/campaigns/announcements", title: "Announcement Campaigns", description: "Send a one-time message to all visitors currently on your site.", section: "campaigns", body: `An announcement is a broadcast message sent simultaneously to every visitor who currently has the Kerabie widget open on your site. It's a real-time megaphone \u2014 useful for time-sensitive updates.
|
|
471
|
-
|
|
472
|
-
<Warning>Announcements go to all active visitors at once. Use them for important, broadly relevant messages \u2014 not personalised outreach.</Warning>
|
|
473
|
-
|
|
474
|
-
## Creating an announcement
|
|
475
|
-
|
|
476
|
-
1. Go to **Campaigns \u2192 New campaign \u2192 Announcement**
|
|
477
|
-
2. Write your **message** \u2014 keep it short (1\u20132 sentences)
|
|
478
|
-
3. Optionally add a **call to action** button (label + URL)
|
|
479
|
-
4. Preview the widget appearance
|
|
480
|
-
5. Click **Send now** or **Schedule**
|
|
481
|
-
|
|
482
|
-
## Scheduling
|
|
483
|
-
|
|
484
|
-
Announcements can be sent immediately or scheduled for a future date and time. Scheduled announcements trigger for visitors who are active at that exact moment \u2014 not visitors who come later.
|
|
485
|
-
|
|
486
|
-
## Examples
|
|
487
|
-
|
|
488
|
-
**New feature launch:**
|
|
489
|
-
> "\u{1F389} Our new AI assistant is live! Ask it anything \u2014 it's trained on our full help centre."
|
|
490
|
-
|
|
491
|
-
**Scheduled maintenance:**
|
|
492
|
-
> "We'll be down for maintenance on Saturday 10\u201311 AM UTC. Chat support will be limited during this time."
|
|
493
|
-
|
|
494
|
-
**Limited-time offer:**
|
|
495
|
-
> "Flash sale: 30% off all plans today only. Use code FLASH30 at checkout."
|
|
496
|
-
|
|
497
|
-
## Delivery
|
|
498
|
-
|
|
499
|
-
The announcement appears as a message in the widget chat \u2014 the same as a message from an agent, but sent by the system. Visitors can reply to it, which starts a live conversation.
|
|
500
|
-
|
|
501
|
-
If a visitor has the widget open but minimised, the widget badge updates to show the new message.
|
|
502
|
-
|
|
503
|
-
## Reach
|
|
504
|
-
|
|
505
|
-
The announcement reaches only visitors **currently on site** with the widget loaded \u2014 it is not an email blast or a push notification. Visitors who arrive after the announcement is sent do not see it.
|
|
506
|
-
|
|
507
|
-
## Analytics
|
|
508
|
-
|
|
509
|
-
After sending, view **Campaigns \u2192 [campaign name]** to see:
|
|
510
|
-
- Total delivered
|
|
511
|
-
- Opened (visitor saw it)
|
|
512
|
-
- Replied (visitor sent a message back)`, headings: [{ level: 2, text: "Creating an announcement" }, { level: 2, text: "Scheduling" }, { level: 2, text: "Examples" }, { level: 2, text: "Delivery" }, { level: 2, text: "Reach" }, { level: 2, text: "Analytics" }] }, { id: "campaigns/billing/invoices", url: "https://kerabie.com/docs/campaigns/billing/invoices", title: "Invoices & Receipts", description: "Download and manage your billing history.", section: "campaigns", body: "## Download invoices\n\nAll invoices are available in **Settings \u2192 Billing \u2192 Invoices**. Each invoice is a PDF you can download and share with your finance team.\n\n## Invoice details\n\nEach invoice includes:\n- Invoice number and date\n- Your organisation name and address\n- Itemised charges (plan, seat count, billing period)\n- Payment method (last 4 digits)\n- Amount paid\n\n## Adding a billing email\n\nBy default invoices are sent to the org owner's email. To send them to a separate billing address (e.g. `finance@yourcompany.com`):\n\nGo to **Settings \u2192 Billing \u2192 Billing email** and enter the address.\n\n## Tax / VAT\n\nTo add a tax ID or company details to your invoices, go to **Settings \u2192 Billing \u2192 Tax information**.\n\n## Failed payments\n\nIf a payment fails, Kerabie retries automatically over several days:\n\n| Attempt | Timing |\n|---|---|\n| Initial charge | Billing date |\n| 1st retry | +2 days |\n| 2nd retry | +4 days |\n| 3rd retry | +7 days |\n\nAfter 3 failures, your subscription is downgraded to Free and you'll receive an email.\n\nTo fix a payment failure, go to **Settings \u2192 Billing \u2192 Update payment method**.", headings: [{ level: 2, text: "Download invoices" }, { level: 2, text: "Invoice details" }, { level: 2, text: "Adding a billing email" }, { level: 2, text: "Tax / VAT" }, { level: 2, text: "Failed payments" }] }, { id: "campaigns/billing/plans", url: "https://kerabie.com/docs/campaigns/billing/plans", title: "Plans & Pricing", description: "Compare Kerabie plans and understand what's included.", section: "campaigns", body: `## Plan comparison
|
|
513
|
-
|
|
514
|
-
| Feature | Free | Starter | Pro | Business |
|
|
515
|
-
|---|---|---|---|---|
|
|
516
|
-
| Agents | 1 | 3 | 10 | Unlimited |
|
|
517
|
-
| Conversations/month | Unlimited | Unlimited | Unlimited | Unlimited |
|
|
518
|
-
| AI chats/month | \u2014 | 200 | 2,000 | 10,000 |
|
|
519
|
-
| Chat widgets | 1 | 2 | 5 | Unlimited |
|
|
520
|
-
| Visitor tracking | \u2014 | \u2014 | \u2713 | \u2713 |
|
|
521
|
-
| Knowledge base articles | 10 | 50 | Unlimited | Unlimited |
|
|
522
|
-
| Remove Kerabie branding | \u2014 | \u2014 | \u2713 | \u2713 |
|
|
523
|
-
| Virtual assistants | \u2014 | \u2713 | \u2713 | \u2713 |
|
|
524
|
-
| Custom automations | \u2014 | 5 | Unlimited | Unlimited |
|
|
525
|
-
| Campaigns | \u2014 | \u2014 | \u2713 | \u2713 |
|
|
526
|
-
| Analytics export | \u2014 | \u2014 | \u2713 | \u2713 |
|
|
527
|
-
| Priority support | \u2014 | \u2014 | \u2014 | \u2713 |
|
|
528
|
-
| SLA | \u2014 | \u2014 | \u2014 | \u2713 |
|
|
529
|
-
|
|
530
|
-
All paid plans include a **14-day free trial**. No credit card required to start.
|
|
531
|
-
|
|
532
|
-
## Billing cycle
|
|
533
|
-
|
|
534
|
-
Plans are billed monthly or yearly. Yearly plans get 2 months free.
|
|
535
|
-
|
|
536
|
-
## What counts as an "AI chat"
|
|
537
|
-
|
|
538
|
-
Each time the AI sends a reply to a visitor counts as one AI chat. If the AI escalates to a human agent, subsequent messages from the agent don't count. One conversation may involve multiple AI chats if the visitor sends several questions.
|
|
539
|
-
|
|
540
|
-
## Overage
|
|
541
|
-
|
|
542
|
-
If you exceed your AI chat limit, the AI stops responding for the rest of the month and conversations go straight to human agents. Upgrade your plan to restore AI \u2014 no automatic overage charges.`, headings: [{ level: 2, text: "Plan comparison" }, { level: 2, text: "Billing cycle" }, { level: 2, text: 'What counts as an "AI chat"' }, { level: 2, text: "Overage" }] }, { id: "campaigns/billing/upgrading", url: "https://kerabie.com/docs/campaigns/billing/upgrading", title: "Upgrading & Downgrading", description: "How to change your plan.", section: "campaigns", body: "## Upgrade\n\n1. Go to **Settings \u2192 Billing \u2192 Plans**\n2. Click **Upgrade** on the plan you want\n3. Enter payment details (card via Paystack)\n4. Confirm \u2014 your new plan is active immediately\n\nWhen you upgrade mid-cycle, you're charged a pro-rated amount for the remaining days.\n\n## Downgrade\n\n1. Go to **Settings \u2192 Billing \u2192 Plans**\n2. Click **Downgrade**\n3. Confirm\n\nDowngrades take effect at the end of your current billing period. You keep your current plan's features until then.\n\n Downgrading may reduce your agent seats, AI chat quota, or widget count. Features above the new plan's limit will be deactivated at the end of the billing period.\n\n## Cancel\n\nGo to **Settings \u2192 Billing \u2192 Cancel subscription**. Your plan continues until the end of the billing period, then switches to Free.\n\nYour data is preserved on the Free plan \u2014 you don't lose conversation history when you cancel.\n\n## Reactivate\n\nIf you cancelled and want to reactivate, just go to **Settings \u2192 Billing \u2192 Plans** and choose a plan. Your previous data is intact.", headings: [{ level: 2, text: "Upgrade" }, { level: 2, text: "Downgrade" }, { level: 2, text: "Cancel" }, { level: 2, text: "Reactivate" }] }, { id: "campaigns/overview", url: "https://kerabie.com/docs/campaigns/overview", title: "Campaigns", description: "Proactively reach visitors with targeted messages.", section: "campaigns", body: "Campaigns let you send messages to visitors proactively \u2014 before they ask for help. Unlike reactive support, campaigns start the conversation on your terms.\n\n<Note>Campaigns are available on **Starter** and above.</Note>\n\n## Campaign types\n\n| Type | What it does |\n|---|---|\n| **Proactive message** | Triggers a message when a visitor spends time on a specific page |\n| **Announcement** | Sends a one-time broadcast to all active visitors at once |\n\n## When to use each type\n\n**Proactive messages** are ideal for:\n- Offering help on a pricing or checkout page before visitors leave\n- Guiding new users through onboarding steps\n- Flagging a known issue on a page affected by downtime\n\n**Announcements** are ideal for:\n- Notifying visitors of a new feature or product launch\n- Sharing a limited-time offer with everyone currently on site\n- Alerting users to scheduled maintenance\n\n## Campaign vs automation\n\nBoth campaigns and automations can trigger messages \u2014 the difference is scope:\n\n- **Automations** react to specific conversation events (conversation created, message received, etc.)\n- **Campaigns** target visitors by page, session behaviour, or a one-time broadcast \u2014 even before a conversation starts\n\n## Getting started\n\n- [Proactive messages](/docs/campaigns/proactive) \u2014 set up page-specific triggers\n- [Announcements](/docs/campaigns/announcements) \u2014 send a broadcast to all active visitors\n\n## Analytics\n\nView campaign performance under **Analytics \u2192 Campaigns**:\n- Messages sent\n- Open rate (visitor saw the message)\n- Reply rate (visitor replied, starting a conversation)\n- Conversion rate (conversation was resolved)", headings: [{ level: 2, text: "Campaign types" }, { level: 2, text: "When to use each type" }, { level: 2, text: "Campaign vs automation" }, { level: 2, text: "Getting started" }, { level: 2, text: "Analytics" }] }, { id: "campaigns/proactive", url: "https://kerabie.com/docs/campaigns/proactive", title: "Proactive Messages", description: "Trigger a message when a visitor spends time on a specific page.", section: "campaigns", body: 'A proactive message is an automated chat message that appears to a visitor based on the page they\'re on and how long they\'ve been there. It starts the conversation before they have to ask.\n\n## Creating a proactive message\n\n1. Go to **Campaigns \u2192 New campaign \u2192 Proactive message**\n2. Set the **trigger conditions**:\n - **Page URL** \u2014 exact match, contains, or starts with\n - **Time on page** \u2014 e.g. after 30 seconds\n - **Visit count** \u2014 e.g. only on first visit, or after 3+ visits\n3. Write the **message** (supports template variables)\n4. Set **frequency** \u2014 show once per session, once per day, or always\n5. Choose whether to show in **widget** or as a **full banner**\n6. Click **Activate**\n\n## Example campaigns\n\n**Help on the pricing page:**\n- Trigger: URL contains `/pricing`, time on page \u2265 20 seconds\n- Message: "Hi! Got questions about our plans? I\'m happy to help."\n- Frequency: Once per session\n\n**Onboarding nudge:**\n- Trigger: URL contains `/app/onboarding`, visit count = 1\n- Message: "Welcome! Need a hand getting set up? It takes about 5 minutes."\n- Frequency: Once per visitor\n\n**Checkout recovery:**\n- Trigger: URL contains `/checkout`, time on page \u2265 60 seconds\n- Message: "Still deciding? We offer a 14-day money-back guarantee \u2014 happy to answer questions."\n- Frequency: Once per day\n\n## Template variables\n\n| Variable | Value |\n|---|---|\n| `{{visitor_name}}` | Visitor\'s name if known |\n| `{{page_title}}` | Current page title |\n\n## Frequency options\n\n| Option | Behaviour |\n|---|---|\n| Once per session | Shown once while the visitor is on the site \u2014 resets when they close the tab |\n| Once per visitor | Shown only once, ever \u2014 stored in a cookie |\n| Once per day | Resets at midnight |\n| Always | Shown every time conditions are met |\n\n## Pausing and stopping\n\nToggle a campaign off from the Campaigns list at any time. Pausing stops new messages immediately \u2014 it doesn\'t affect conversations already started.', headings: [{ level: 2, text: "Creating a proactive message" }, { level: 2, text: "Example campaigns" }, { level: 2, text: "Template variables" }, { level: 2, text: "Frequency options" }, { level: 2, text: "Pausing and stopping" }] }, { id: "changelog", url: "https://kerabie.com/docs/changelog", title: "Changelog", description: "What's new in Kerabie.", section: "changelog", body: "## October 2026\n\n### Redesigned dashboard\nThe dashboard and Conversations page have a new look, with \u2318K search across conversations and contacts.\n\n- **Dashboard:** period-over-period changes on each number, an AI-vs-human conversation chart (7 or 30 days), the live queue, open tickets with SLA, team presence, and a setup checklist that now reflects what you've actually connected.\n- **AI card:** share of conversations Kerabie AI resolved without an agent, share handed off, and an estimate of agent-hours saved.\n- **Conversations:** status tabs with counts, tags on each row, an internal-note tab, an on-demand AI suggested reply and AI summary, previous conversations for the same customer, and **Take over** when Kerabie AI is handling a chat.\n- **Search:** conversation search is now case-insensitive.\n- **Tickets:** a new Tickets page with an overview strip, List and Board views, bulk actions, CSV export (Pro and above), agent-created tickets, and tags you can create on the spot. A ticket with no customer email now says so, with a quick way to add one.\n- **Analytics:** the Analytics page is redesigned with six tabs: Overview, Agents, Channels, AI and questions, Customers and Pages. New: median first-response time with change vs the previous period, per-agent and per-channel response times and CSAT, the questions customers ask most (with how often the AI answered alone and the customer's rating), questions the AI couldn't answer, new vs returning customers, top countries and customers, a busiest-times map in your timezone, and which pages start the most conversations. Export the report as CSV on Pro and up. Available in the API and SDK as `analytics.getInsights()`.\n- **Campaigns:** a redesigned Campaigns page with draft, active and paused states, a funnel per campaign (shown, opened, clicked, replied), a button with an action (open chat, open the Knowledge Base or a link), and rules for pages, audience (new, returning, hasn't chatted, or visitors from a chosen country) and frequency. New triggers: scroll depth and page load, alongside time on page and exit intent. Duplicate a campaign as a draft. How many can be live at once depends on your plan. Campaigns also appear in React Native apps (page-load and time-in-app triggers) and can be managed from the mobile app. Available in the API and SDK as `campaigns` (`list`, `getStats`, `get`, `create`, `update`, `duplicate`, `delete`).\n- **Contacts:** type (lead, customer, VIP), company, owner, notes and tags on every contact, a last-contact time, overview numbers, search across name, email, company and tags, add by hand, and CSV import (bad rows are reported by number, existing emails skipped). Contact tags use the same tags as conversations and tickets, and can be created on the spot.\n- **Visitors:** the Visitors page now shows recent visitors as well as live ones, with visit counts, returning visitors, top countries, block and allow status, and CSV export (Pro and above). Visitor history is kept for 30 days. *Save as contact* now works for visitors who never sent a message.\n\n- **AI Assistant:** a redesigned AI Assistant page with Setup, Guardrails and handoff, and Test chat tabs. Set the reply language and length, choose guardrails (only use Knowledge Base facts, never ask for sensitive details, refunds need a person, allow discounts), block topics, and set a minimum Knowledge Base match so the AI hands off questions your articles don't cover. Choose which team gets AI handoffs, what the AI says outside business hours, and whether the agent gets an AI summary. The AI can now answer on WhatsApp, Instagram and email (off by default, per channel), and agent replies on WhatsApp and Instagram are now delivered to the customer. Try changes in Test chat before saving. Also in the mobile app.\n- **AI Tools:** a redesigned AI Tools page with usage and success rate per tool, templates, a quality hint for descriptions, and a real test that shows what the AI would see. Duplicate a tool (the copy starts switched off). How many tools you can have depends on your plan. Endpoints must be public addresses, and secrets are never shown again after saving. Available in the API and SDK as `aiTools` (`list`, `get`, `create`, `update`, `duplicate`, `delete`, `test`) and in the mobile app. See the [AI Tools API](/docs/api-reference/ai-tools).\n\n- **Knowledge Base:** a redesigned Knowledge Base page with search, filters (type, status, category), sorting and pagination, grid and list views, a preview of how customers see an article, and an Analytics tab: most viewed articles, customer questions the AI couldn't answer (what to write next), lowest rated articles and articles that need a refresh. Each article shows its views, how helpful visitors found it and how often the AI cited it. The chat widget and the React Native package now ask \"Was this helpful?\" under articles. Import accepts CSV (one article per row, no AI needed), Word and Markdown files, and reports the rows it skipped. Available in the API and SDK as `kb.getStats()` and `kb.getInsights()`, and in the mobile app.\n- **MCP:** two ways to use the Model Context Protocol. **Kerabie MCP server**: connect Claude, Cursor or any MCP client to Kerabie. Without an API key it helps you build: search the docs, list API endpoints, read the TypeScript and React Native SDK types and follow integration guides. With a key it works with your account (conversations, tickets, contacts, campaigns, the Knowledge Base, analytics and AI tools), with a read-only mode. **MCP servers as tool sources:** on the AI Tools page (web and mobile), connect a remote MCP server and switch on the tools your chatbot may use; each counts towards your plan's AI tool limit. Available in the API and SDK as `mcpServers`. See [MCP server](/docs/sdks/mcp) and [Connecting MCP servers](/docs/ai/mcp-servers).\n\n### API and SDK\nNew in `@kerabie/sdk` and the `/v1` API: `analytics.getTicketSla()`, `analytics.getSetupStatus()`, `conversations.getHistory()`, `conversations.takeOver()`, `tickets.getStats()`, `tickets.export()`, `visitors.list()`, `visitors.getOverview()`, `visitors.export()` and a new `contacts` resource (`list`, `getStats`, `get`, `create`, `update`, `import`). `analytics.getSummary()` now returns `deltas`, `ai` and a daily AI/human `trend`; `conversations.list()` returns tags, `aiHandling` and per-status `meta.counts`. See the [Analytics](/docs/api-reference/analytics) and [Conversations](/docs/api-reference/conversations) references.\n\nThe analytics response types in earlier SDK builds did not match the API, and the `/v1/analytics` and `/v1/conversations` routes they called were not available; both now match.\n\n---\n\n## May 2026\n\n### Admin Portal\nBuilt the internal admin portal for Kerabie staff. Includes:\n- User and org management\n- Billing dashboard (MRR, churn, failed payments)\n- Platform analytics\n- VA marketplace moderation\n- System health monitoring\n\n### Help Center\nLaunched the public Help Center at [kerabie.com/help-center](https://kerabie.com/help-center). 56 articles across 9 categories: Getting Started, Conversations, Agents & Teams, Visitors, AI & Chatbot, Billing & Plans, Widget, Security, and Notifications.\n\n### Offline Email Notifications\nWhen a visitor starts a conversation and no agents are online, the org owner now receives an immediate email notification.\n\n### Trial Warning Emails\nOrganisations on a trial now receive a 7-day advance warning email before their trial expires.\n\n### Analytics \u2014 Live Visitor Count\nThe analytics dashboard now shows a real-time active visitor count (previously always showed 0).\n\n---\n\n## April 2026\n\n### AI Chatbot\nReleased the AI chatbot powered by your knowledge base. Handles common questions automatically with configurable confidence threshold and human escalation.\n\n### Knowledge Base\nAdded the Knowledge Base feature \u2014 create, organise, and publish articles. Articles feed the AI and appear in the widget's FAQ tab.\n\n### Webhooks\nLaunched the webhooks system. Receive real-time events (`conversation.created`, `message.sent.*`, `visitor.*`) with HMAC signature verification and automatic retry.\n\n### Virtual Assistant Marketplace\nLaunched the VA marketplace. Businesses can browse, hire, and manage verified virtual assistants directly from the dashboard.\n\n---\n\n## March 2026\n\n### Chat Widget\nLaunched the embeddable chat widget \u2014 install via script tag, WordPress plugin, or React Native SDK.\n\n### Real-time Conversations\nWebSocket-powered real-time messaging between visitors and agents.\n\n### Push Notifications\nMobile push notifications via Expo Push for iOS and Android agents.\n\n### Multi-language Support\nWidget UI available in 40+ languages. Auto-detected from browser locale.", headings: [{ level: 2, text: "October 2026" }, { level: 3, text: "Redesigned dashboard" }, { level: 3, text: "API and SDK" }, { level: 2, text: "May 2026" }, { level: 3, text: "Admin Portal" }, { level: 3, text: "Help Center" }, { level: 3, text: "Offline Email Notifications" }, { level: 3, text: "Trial Warning Emails" }, { level: 3, text: "Analytics \u2014 Live Visitor Count" }, { level: 2, text: "April 2026" }, { level: 3, text: "AI Chatbot" }, { level: 3, text: "Knowledge Base" }, { level: 3, text: "Webhooks" }, { level: 3, text: "Virtual Assistant Marketplace" }, { level: 2, text: "March 2026" }, { level: 3, text: "Chat Widget" }, { level: 3, text: "Real-time Conversations" }, { level: 3, text: "Push Notifications" }, { level: 3, text: "Multi-language Support" }] }, { id: "conversations/assigning", url: "https://kerabie.com/docs/conversations/assigning", title: "Assigning Conversations", description: "Manually or automatically assign conversations to agents or teams.", section: "conversations", body: "Conversations can be assigned to a specific agent or team so the right person handles each request.\n\n## Manual assignment\n\n1. Open a conversation\n2. In the right-hand panel, find **Assigned to**\n3. Click **Assign** and pick an agent or team from the list\n4. The agent receives a notification and the conversation appears in their queue\n\n## Automatic assignment via Automations\n\nUse automations to route conversations without manual work:\n\n1. Go to **Settings \u2192 Automations \u2192 New automation**\n2. Set trigger: `Conversation created`\n3. Add condition (optional): e.g. page URL, visitor attribute, or message keyword\n4. Set action: `Assign agent` or `Assign team`\n\nExample \u2014 route pricing enquiries to Sales:\n- **Trigger**: Conversation created\n- **Condition**: Page URL contains `/pricing`\n- **Action**: Assign to team \u2192 Sales\n\nSee [Automations](/docs/automations/overview) for the full reference.\n\n## Round-robin team assignment\n\nWhen a conversation is assigned to a **team**, Kerabie distributes it to the next available agent using round-robin. Agents must be set as **Available** to receive new conversations.\n\n## Unassigned conversations\n\nConversations with no assignee appear in the **Unassigned** filter in the inbox. Supervisors can monitor this view and redistribute work.\n\n## Reassigning\n\nClick the current assignee in the right panel and choose a different agent. The conversation keeps its current status.", headings: [{ level: 2, text: "Manual assignment" }, { level: 2, text: "Automatic assignment via Automations" }, { level: 2, text: "Round-robin team assignment" }, { level: 2, text: "Unassigned conversations" }, { level: 2, text: "Reassigning" }] }, { id: "conversations/history-export", url: "https://kerabie.com/docs/conversations/history-export", title: "History & Export", description: "Search, filter, and export conversation history.", section: "conversations", body: "All conversations \u2014 including resolved and closed ones \u2014 are stored and searchable. Export them for reporting, audits, or CRM imports.\n\n## Viewing history\n\nIn the **Conversations** inbox, switch to the **History** tab to see resolved and closed conversations. Filter by:\n\n- Visitor name or email\n- Message content keyword\n- Date range\n- Assigned agent or team\n- Tags\n\n## Exporting to CSV\n\n<Note>CSV export is available on **Pro** and above.</Note>\n\n1. Go to **Analytics \u2192 Conversations**\n2. Set the date range and filters\n3. Click **Export \u2192 CSV**\n\n### Export fields\n\n| Field | Description |\n|---|---|\n| `id` | Unique conversation ID |\n| `visitor_name` | Visitor's name if collected |\n| `visitor_email` | Visitor's email if collected |\n| `status` | Final status at export time |\n| `assigned_to` | Agent email |\n| `created_at` | First message timestamp |\n| `resolved_at` | Resolution timestamp |\n| `message_count` | Total messages |\n| `tags` | Comma-separated tags |\n| `rating` | CSAT rating if collected |\n\n## Retention\n\nConversations are retained indefinitely on all paid plans. Free plan conversations older than 90 days are archived and not directly searchable.\n\n## Data deletion (GDPR)\n\nTo erase a visitor's data:\n\n1. Find the visitor in **Contacts**\n2. Click **Delete contact**\n3. Their profile is removed and conversation messages are anonymised", headings: [{ level: 2, text: "Viewing history" }, { level: 2, text: "Exporting to CSV" }, { level: 3, text: "Export fields" }, { level: 2, text: "Retention" }, { level: 2, text: "Data deletion (GDPR)" }] }, { id: "conversations/overview", url: "https://kerabie.com/docs/conversations/overview", title: "Conversations", description: "How conversations work in Kerabie.", section: "conversations", body: "A conversation starts when a visitor sends their first message through the widget. Every conversation has a status, an optional assigned agent, and a full message history.\n\n## Conversation lifecycle\n\n```\nVisitor opens widget \u2192 sends message\n \u2193\nConversation created \u2192 status: WAITING\n \u2193\nAgent picks up \u2192 status: ACTIVE (or AI handles it)\n \u2193\nIssue resolved \u2192 status: RESOLVED\n \u2193\nAfter 24h of no activity \u2192 status: CLOSED\n```\n\n## Statuses\n\n| Status | Meaning |\n|---|---|\n| `WAITING` | New \u2014 no agent has responded yet |\n| `ACTIVE` | Agent or AI is handling it |\n| `RESOLVED` | Marked resolved by an agent |\n| `CLOSED` | Automatically closed after inactivity |\n\n## The inbox\n\nThe **Conversations** page in the dashboard is your inbox. It shows:\n\n- All open conversations, sorted by last message\n- Unread count badge per conversation\n- Filter by status, agent, tag, or date\n- Search across message content and customer details\n\n## Assigning conversations\n\nConversations can be assigned to specific agents or teams:\n\n- **Manual**: Click **Assign** in any open conversation\n- **Automatic**: Use [Automations](/automations/overview) to route based on rules (page URL, visitor country, keyword, etc.)\n- **Round-robin**: Enable in **Settings \u2192 Routing** to distribute conversations equally across online agents\n\n## Tags\n\nAdd tags to conversations for organisation and reporting. Tags can be added manually or automatically via automations. Examples: `billing`, `bug-report`, `feature-request`, `vip`.", headings: [{ level: 2, text: "Conversation lifecycle" }, { level: 2, text: "Statuses" }, { level: 2, text: "The inbox" }, { level: 2, text: "Assigning conversations" }, { level: 2, text: "Tags" }] }, { id: "conversations/quick-responses", url: "https://kerabie.com/docs/conversations/quick-responses", title: "Quick Responses", description: "Save and reuse templated replies for common questions.", section: "conversations", body: "Quick responses (canned replies) are pre-written messages agents can insert with a keystroke, eliminating repetitive typing for common questions.\n\n## Creating a quick response\n\n1. Go to **Settings \u2192 Widget Settings \u2192 Quick Responses**\n2. Click **New response**\n3. Enter a **shortcut** (e.g. `greet`) and the **message body**\n4. Save\n\n## Using a quick response\n\nIn the reply box, type `/` followed by the shortcut (e.g. `/greet`). A dropdown shows matching responses \u2014 click to insert, then edit before sending if needed.\n\nYou can also click the **\u26A1 lightning bolt** icon in the reply toolbar to browse all responses.\n\n## Template variables\n\nThese placeholders are replaced automatically when the response is inserted:\n\n| Variable | Replaced with |\n|---|---|\n| `{{visitor_name}}` | The visitor's name (if known) |\n| `{{agent_name}}` | The logged-in agent's display name |\n| `{{org_name}}` | Your organisation name |\n\nExample:\n```\nHi {{visitor_name}}, I'm {{agent_name}} from {{org_name}}. How can I help?\n```\n\n## Tips\n\n- Keep shortcuts short: `greet`, `sorry`, `refund`, `close`, `hours`\n- Write in a neutral tone agents can personalise before sending\n- Only admins can create or delete responses \u2014 all agents can use them", headings: [{ level: 2, text: "Creating a quick response" }, { level: 2, text: "Using a quick response" }, { level: 2, text: "Template variables" }, { level: 2, text: "Tips" }] }, { id: "conversations/statuses", url: "https://kerabie.com/docs/conversations/statuses", title: "Conversation Statuses", description: "The four statuses a conversation moves through: WAITING, ACTIVE, RESOLVED, and CLOSED.", section: "conversations", body: 'Every conversation in Kerabie has a status that reflects where it is in its lifecycle.\n\n## The four statuses\n\n| Status | Meaning |\n|---|---|\n| **Waiting** | A visitor sent a message and no agent has replied yet |\n| **Active** | An agent is engaged \u2014 the conversation is open and in progress |\n| **Resolved** | The issue was handled \u2014 the conversation is closed but retrievable |\n| **Closed** | Permanently archived \u2014 visible in history but removed from the active queue |\n\n## Status transitions\n\n```\nVisitor sends message\n \u2193\n WAITING \u2500\u2500\u2500\u2500 agent replies \u2500\u2500\u2500\u2500\u2192 ACTIVE\n \u2193 \u2193\n (auto-resolve agent resolves\n after timeout) \u2193\n RESOLVED\n \u2193\n (manual close\n or auto-close)\n \u2193\n CLOSED\n```\n\n## Changing status\n\nIn the conversation panel, use the status dropdown in the top-right corner:\n\n- **Resolve** \u2014 marks the conversation resolved and removes it from the main queue\n- **Close** \u2014 archives it permanently\n- **Reopen** \u2014 moves a resolved conversation back to Active\n\n## Auto-resolve\n\nYou can configure Kerabie to automatically resolve conversations after a period of inactivity:\n\n1. Go to **Settings \u2192 Widget Settings \u2192 Auto-resolve**\n2. Set the inactivity threshold (e.g. 24 hours)\n3. Save\n\nConversations that are auto-resolved appear with a "System" label in the activity log.\n\n## Filtering by status\n\nIn the **Conversations** inbox, use the status filter buttons at the top to view:\n\n- All conversations\n- Waiting only\n- Active only\n- Resolved\n\nClosed conversations are available under **History**.', headings: [{ level: 2, text: "The four statuses" }, { level: 2, text: "Status transitions" }, { level: 2, text: "Changing status" }, { level: 2, text: "Auto-resolve" }, { level: 2, text: "Filtering by status" }] }, { id: "getting-started/add-to-website", url: "https://kerabie.com/docs/getting-started/add-to-website", title: "Add to Your Website", description: "Install the Kerabie widget on any website.", section: "getting-started", body: "See the [Quickstart](/docs/quickstart) for the fastest path, or choose your platform:\n\n- [HTML script tag](/docs/widget/install-html) \u2014 any site, paste two lines of code\n- [WordPress](/docs/widget/install-wordpress) \u2014 install the plugin, no code required\n- [Webflow / Squarespace / Wix](/docs/widget/install-webflow) \u2014 no-code builders\n- [Shopify](/docs/widget/install-shopify) \u2014 e-commerce stores\n- [React Native](/docs/sdks/react-native) \u2014 iOS and Android apps", headings: [] }, { id: "getting-started/first-conversation", url: "https://kerabie.com/docs/getting-started/first-conversation", title: "Your First Conversation", description: "Test that everything is working end to end.", section: "getting-started", body: "After installing the widget:\n\n1. Open your site in a browser\n2. Click the chat bubble in the corner\n3. Type a test message and hit send\n\nNow open the Kerabie dashboard \u2192 **Conversations**. Your test message appears in real time.\n\nClick **Reply**, type a response, and send \u2014 it appears in the widget immediately.\n\n If you see the exchange in both places, everything is working correctly.\n\n## Troubleshooting\n\n**Widget doesn't appear**\n- Check the widget key in your script matches the one in Settings \u2192 Widget\n- Clear your browser cache and hard-reload (`Ctrl+Shift+R`)\n- Check for JavaScript errors in the browser console\n\n**Message not showing in dashboard**\n- Make sure you're logged in to the right Kerabie account\n- Check you're viewing the correct organisation if you have multiple\n\n**Still stuck?** Open a conversation via the widget on [kerabie.com](https://kerabie.com) and we'll help.", headings: [{ level: 2, text: "Troubleshooting" }] }, { id: "getting-started/invite-agents", url: "https://kerabie.com/docs/getting-started/invite-agents", title: "Invite Your First Agent", description: "Add team members to your Kerabie workspace.", section: "getting-started", body: "## Invite an agent\n\n1. Go to **Settings \u2192 Agents \u2192 Invite agent**\n2. Enter their email address\n3. Choose their role: **Agent** or **Admin**\n4. Click **Send invite**\n\nThey'll receive an email with a link to join your workspace. If they don't have a Kerabie account yet, they'll be prompted to create one.\n\n## Roles\n\n| Role | Can do |\n|---|---|\n| **Agent** | View and reply to conversations, manage their own settings |\n| **Admin** | Everything agents can do + invite agents, change widget settings, view billing |\n\n## Alternative: Hire a virtual assistant\n\nDon't have in-house support staff? Browse verified virtual assistants on the [VA Marketplace](/agents/virtual-assistants) and hire one directly from the dashboard.", headings: [{ level: 2, text: "Invite an agent" }, { level: 2, text: "Roles" }, { level: 2, text: "Alternative: Hire a virtual assistant" }] }, { id: "index", url: "https://kerabie.com/docs/", title: "What is Kerabie?", description: "Live chat, AI support, and virtual assistants \u2014 all in one platform.", section: "index", body: 'Kerabie is a customer support platform that lets you add live chat to any website in under 5 minutes. It combines real-time messaging, an AI chatbot powered by your knowledge base, and an on-demand marketplace of verified virtual assistants.\n\n## What you can do\n\n <Card title="Live Chat" icon="message" href="/widget/overview">\n Add a chat widget to your site. Visitors chat, agents reply \u2014 in real time.\n </Card>\n <Card title="AI Chatbot" icon="robot" href="/ai/overview">\n Connect your knowledge base to an AI that handles common questions automatically.\n </Card>\n <Card title="Virtual Assistants" icon="users" href="/agents/virtual-assistants">\n Hire verified support agents from the Kerabie marketplace when you need extra help.\n </Card>\n <Card title="Automations" icon="bolt" href="/automations/overview">\n Set rules to route conversations, trigger messages, and assign agents automatically.\n </Card>\n\n## How it works\n\n1. **Install the widget** \u2014 paste one script tag into your site\'s `<head>`. No framework required.\n2. **Customize** \u2014 set your brand colors, welcome message, and business hours.\n3. **Connect AI** \u2014 add your FAQ articles or import your existing docs. The AI answers immediately.\n4. **Invite agents** \u2014 add your team or hire a virtual assistant from the marketplace.\n5. **Track everything** \u2014 conversations, response times, satisfaction scores, and visitor behavior in one dashboard.\n\n## Plans\n\n| Feature | Free | Starter | Pro | Business |\n|---|---|---|---|---|\n| Agents | 1 | 3 | 10 | Unlimited |\n| AI chats/month | \u2014 | 200 | 2,000 | 10,000 |\n| Visitor tracking | \u2014 | \u2014 | \u2713 | \u2713 |\n| Custom branding removal | \u2014 | \u2014 | \u2713 | \u2713 |\n| Virtual assistants | \u2014 | \u2713 | \u2713 | \u2713 |\n\n All plans include unlimited conversations. Limits apply to agents and AI usage.\n\n## Next steps\n\n <Card title="5-minute quickstart" icon="rocket" href="/quickstart">\n Get your first conversation in under 5 minutes.\n </Card>\n <Card title="API Reference" icon="code" href="/api-reference/overview">\n Build custom integrations with the REST API.\n </Card>', headings: [{ level: 2, text: "What you can do" }, { level: 2, text: "How it works" }, { level: 2, text: "Plans" }, { level: 2, text: "Next steps" }] }, { id: "integrations/overview", url: "https://kerabie.com/docs/integrations/overview", title: "Integrations", description: "Connect Kerabie to your other tools and platforms.", section: "integrations", body: 'Kerabie integrates with your website, mobile apps, and third-party services so you can manage all customer conversations in one place.\n\n## Available integrations\n\n <Card title="WordPress" href="/docs/integrations/wordpress">\n Install the official plugin with one click \u2014 no code required.\n </Card>\n <Card title="React Native" href="/docs/integrations/react-native">\n Embed the chat widget in your iOS or Android app.\n </Card>\n <Card title="Webhooks" href="/docs/integrations/webhooks">\n Receive real-time HTTP POST events for conversations, messages, and visitors.\n </Card>\n <Card title="REST API" href="/docs/api-reference/overview">\n Build custom integrations using the Kerabie REST API.\n </Card>\n\n## Coming soon\n\n| Integration | Status |\n|---|---|\n| WhatsApp Business | In development |\n| Instagram DM | In development |\n| Facebook Messenger | Planned |\n| Slack | Planned |\n| Zapier | Planned |\n\nVote on upcoming integrations or request a new one via the [Feature Request](/feature-request) page.\n\n## Custom integrations\n\nUse the [REST API](/docs/api-reference/overview) and [Webhooks](/docs/integrations/webhooks) to build any integration not listed above. The API gives you full read/write access to conversations, visitors, agents, and knowledge base content.\n\n## Managing integrations\n\nAll active integrations are visible under **Settings \u2192 Integrations** in your dashboard. From there you can:\n\n- Generate and revoke API keys\n- Create and manage webhook endpoints\n- Configure allowed widget domains', headings: [{ level: 2, text: "Available integrations" }, { level: 2, text: "Coming soon" }, { level: 2, text: "Custom integrations" }, { level: 2, text: "Managing integrations" }] }, { id: "integrations/react-native", url: "https://kerabie.com/docs/integrations/react-native", title: "React Native SDK", description: "Add Kerabie live chat to your iOS or Android app.", section: "integrations", body: 'The Kerabie React Native SDK lets you embed a fully functional chat widget inside your iOS or Android app with a few lines of code.\n\n## Installation\n\n```bash\nnpm install react-native-kerabie-helpdesk\n```\n\nFor Expo managed workflow, also run:\n\n```bash\nnpx expo install react-native-kerabie-helpdesk\n```\n\n## Basic setup\n\nWrap your app (or just the screen where you want chat) with the provider:\n\n```tsx\n\nexport default function App() {\n return (\n <KerAbieProvider widgetKey="wk_yourkey_here">\n {/* your app */}\n </KerAbieProvider>\n );\n}\n```\n\n## Opening the chat widget\n\n```tsx\n\nfunction SupportButton() {\n const { openChat } = useKerabie();\n return <Button title="Chat with us" onPress={openChat} />;\n}\n```\n\n## Identifying the user\n\nPass the logged-in user\'s details so agents know who they\'re talking to:\n\n```tsx\n\n```\n\n## Configuration options\n\n| Prop | Type | Description |\n|---|---|---|\n| `widgetKey` | `string` | Required \u2014 your Widget Key |\n| `user` | `object` | Optional \u2014 `name`, `email`, `userId` |\n| `primaryColor` | `string` | Override the widget accent colour |\n| `locale` | `string` | Language code (e.g. `en`, `fr`, `es`) |\n\n## Getting your Widget Key\n\n1. Go to [kerabie.com/app](https://kerabie.com/app)\n2. **Settings \u2192 Chat Widget**\n3. Copy the **Widget Key** \u2014 starts with `wk_`\n\n## Full SDK reference\n\nFor the complete API including events, methods, and advanced configuration, see the [TypeScript SDK docs](/docs/sdks/typescript).', headings: [{ level: 2, text: "Installation" }, { level: 2, text: "Basic setup" }, { level: 2, text: "Opening the chat widget" }, { level: 2, text: "Identifying the user" }, { level: 2, text: "Configuration options" }, { level: 2, text: "Getting your Widget Key" }, { level: 2, text: "Full SDK reference" }] }, { id: "integrations/webhooks", url: "https://kerabie.com/docs/integrations/webhooks", title: "Webhooks", description: "Receive real-time event notifications from Kerabie to your server.", section: "integrations", body: 'Webhooks let Kerabie POST to your server whenever something happens \u2014 a new conversation, a message, a resolved ticket. Use them to sync with your CRM, trigger Slack alerts, or run any custom workflow.\n\n<Info>Available on Starter, Pro, and Business plans.</Info>\n\n## Setup\n\n1. Go to **Settings \u2192 API & Webhooks \u2192 Webhooks**\n2. Click **Add Endpoint**\n3. Enter your HTTPS URL (must be publicly reachable)\n4. Select which events to receive\n5. Save \u2014 copy the **Webhook Secret** shown once\n\n## Events\n\n| Event | When it fires |\n|---|---|\n| `conversation.created` | A visitor opens a new conversation |\n| `conversation.resolved` | A conversation is marked resolved |\n| `conversation.closed` | A conversation is closed |\n| `message.sent.agent` | An agent sends a message |\n| `message.sent.customer` | A visitor sends a message |\n| `visitor.started` | A new visitor session begins |\n| `visitor.page_view` | A visitor views a page *(Pro)* |\n\n## Payload format\n\n```json\n{\n "event": "message.sent.agent",\n "orgId": 42,\n "timestamp": "2026-05-26T14:32:00.000Z",\n "data": {\n "conversationId": 101,\n "messageId": 5231,\n "body": "Hi there! How can I help?",\n "agentId": 7,\n "agentName": "Sarah"\n }\n}\n```\n\n## Verifying the signature\n\nEvery request includes headers to confirm it came from Kerabie:\n\n```http\nX-Kerabie-Event: message.sent.agent\nX-Kerabie-Signature: sha256=abc123...\nX-Kerabie-Timestamp: 1716733920\n```\n\n <Tab title="Node.js (manual)">\n ```javascript\n const crypto = require(\'crypto\');\n\n function verifyWebhook(rawBody, signature, secret) {\n const expected = \'sha256=\' + crypto\n .createHmac(\'sha256\', secret)\n .update(rawBody)\n .digest(\'hex\');\n return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));\n }\n\n app.post(\'/webhooks/kerabie\', express.raw({ type: \'application/json\' }), (req, res) => {\n const valid = verifyWebhook(\n req.body,\n req.headers[\'x-kerabie-signature\'],\n process.env.KERABIE_WEBHOOK_SECRET,\n );\n if (!valid) return res.status(401).send(\'Invalid signature\');\n\n const event = JSON.parse(req.body);\n // handle event.event, event.data ...\n res.json({ received: true });\n });\n ```\n </Tab>\n <Tab title="Node.js (SDK)">\n ```typescript\n import { KerAbieClient } from \'@kerabie/sdk\';\n\n app.post(\'/webhooks/kerabie\', express.raw({ type: \'application/json\' }), (req, res) => {\n const isValid = KerAbieClient.webhooks.verify(\n req.body,\n req.headers[\'x-kerabie-signature\'] as string,\n process.env.KERABIE_WEBHOOK_SECRET!,\n );\n if (!isValid) return res.status(401).send(\'Invalid signature\');\n\n const event = KerAbieClient.webhooks.parse(req.body);\n console.log(event.event, event.data);\n res.json({ received: true });\n });\n ```\n </Tab>\n\n## Retry policy\n\nIf your endpoint returns non-2xx or times out (> 30 s), Kerabie retries automatically:\n\n| Attempt | Delay |\n|---|---|\n| 1st retry | 5 seconds |\n| 2nd retry | 30 seconds |\n| 3rd retry | 2 minutes |\n| 4th retry | 10 minutes |\n| 5th retry | 30 minutes |\n\nAfter 5 failures the delivery is marked failed. View logs in **Settings \u2192 Webhooks \u2192 Delivery Logs**.\n\n## Best practices\n\n- Respond `200 OK` immediately, then process async (push to a queue).\n- Make your handler **idempotent** \u2014 the same event may arrive more than once.\n- Reject events whose `timestamp` is more than 5 minutes old to block replay attacks.', headings: [{ level: 2, text: "Setup" }, { level: 2, text: "Events" }, { level: 2, text: "Payload format" }, { level: 2, text: "Verifying the signature" }, { level: 2, text: "Retry policy" }, { level: 2, text: "Best practices" }] }, { id: "integrations/wordpress", url: "https://kerabie.com/docs/integrations/wordpress", title: "WordPress", description: "Install Kerabie on WordPress with the official plugin.", section: "integrations", body: "The Kerabie WordPress plugin adds the live chat widget to your site in minutes \u2014 no code required.\n\n## Installation\n\n### Option A \u2014 from the WordPress plugin directory\n\n1. In WP Admin, go to **Plugins \u2192 Add New**\n2. Search for **Kerabie Live Chat**\n3. Click **Install Now**, then **Activate**\n\n### Option B \u2014 manual upload\n\n1. Go to **Settings \u2192 Integrations \u2192 Channels** in your Kerabie dashboard\n2. Click **WordPress Plugin \u2192 View Setup** to find the download link\n3. In WP Admin, go to **Plugins \u2192 Add New \u2192 Upload Plugin**\n4. Upload the ZIP and activate\n\n## Connecting your account\n\nAfter activation:\n\n1. In WP Admin, go to **Kerabie \u2192 Settings**\n2. Enter your **Widget Key** (find it in Kerabie under **Settings \u2192 Chat Widget**)\n3. Save settings\n\nThe widget appears on all pages immediately.\n\n## Configuration options\n\n| Option | Description |\n|---|---|\n| **Widget Key** | Required \u2014 links the plugin to your Kerabie account |\n| **Show on pages** | All pages, or specific page IDs |\n| **Hide for logged-out users** | Optional \u2014 only show to authenticated WP users |\n| **Pass user data** | If enabled, passes the logged-in WP user's name and email to Kerabie automatically |\n\n## Passing user identity (optional)\n\nEnable **Pass user data** in the plugin settings to automatically identify logged-in WordPress users. Kerabie will show their name and email in the conversation, so agents know who they're talking to without asking.\n\n## Troubleshooting\n\n**Widget not appearing:**\n- Check that your Widget Key is correct (no extra spaces)\n- Ensure another plugin isn't blocking script loading (e.g. cookie consent or caching plugins \u2014 add `cdn.kerabie.com` to the allowlist)\n- Try disabling other plugins temporarily to check for conflicts\n\n**Caching plugins:**\nAfter installing, clear your cache in WP Rocket, W3 Total Cache, or your hosting provider's cache dashboard.", headings: [{ level: 2, text: "Installation" }, { level: 3, text: "Option A \u2014 from the WordPress plugin directory" }, { level: 3, text: "Option B \u2014 manual upload" }, { level: 2, text: "Connecting your account" }, { level: 2, text: "Configuration options" }, { level: 2, text: "Passing user identity (optional)" }, { level: 2, text: "Troubleshooting" }] }, { id: "knowledge-base/analytics", url: "https://kerabie.com/docs/knowledge-base/analytics", title: "Knowledge Base analytics", description: "See what customers read, what they can't find, and which articles to fix.", section: "knowledge-base", body: "The **Analytics** tab on the Knowledge Base page turns your articles and your customers' conversations into a to-do list.\n\n## Most viewed\n\nThe five published articles customers open most, with a bar relative to the top one. These are the ones worth keeping accurate.\n\n## Questions with no answer\n\nCustomer questions from the last 30 days that Kerabie AI had no article to answer from, with how often each was asked. Choose **Write** to start a new article with the question as its title. This is the quickest way to improve how many conversations the AI resolves on its own.\n\n## Helpfulness\n\nUnder each article in the chat widget and the mobile SDK, customers can answer **Was this helpful?** Articles with at least 3 votes are listed lowest first, so you can see which answers confuse people. Fixing them improves the AI's answers too, because it reads the same text.\n\nA customer can vote up to three times per article per day.\n\n## Needs a refresh\n\nPublished articles that haven't been edited for 90 days, oldest first, with how long it has been. Open one to check it is still true.\n\n## From code\n\nThe same numbers are available as `kerabie.kb.getStats()` and `kerabie.kb.getInsights()`. See the [Knowledge Base API](/docs/api-reference/knowledge-base).", headings: [{ level: 2, text: "Most viewed" }, { level: 2, text: "Questions with no answer" }, { level: 2, text: "Helpfulness" }, { level: 2, text: "Needs a refresh" }, { level: 2, text: "From code" }] }, { id: "knowledge-base/articles", url: "https://kerabie.com/docs/knowledge-base/articles", title: "Writing Articles", description: "Create, edit, publish and organise Knowledge Base articles.", section: "knowledge-base", body: `Your Knowledge Base holds the FAQs, articles and news that customers read in the chat widget and that Kerabie AI answers from. Publish an article and the AI can use it straight away; there is no separate training step.
|
|
543
|
-
|
|
544
|
-
## Create an article
|
|
545
|
-
|
|
546
|
-
1. Open **Knowledge Base** and choose **New article**.
|
|
547
|
-
2. Enter a **title**, for example *How to reset your password*.
|
|
548
|
-
3. Choose the **type** (Article, FAQ or News), the **category** and the **status**.
|
|
549
|
-
4. Write the answer in the editor. Short paragraphs work best, and the counter under the editor tells you if it's too short for the AI to cite well or long enough to split.
|
|
550
|
-
5. Choose **Publish** to make it live, or **Save draft** to keep working. Drafts and archived articles are never shown to customers or used by the AI.
|
|
551
|
-
|
|
552
|
-
The editor supports headings, bold, italic, underline, bullet and numbered lists, links and code blocks. You can also add a **cover photo** (cropped to 16:9) once the article has been saved, and use **Refine with AI** to tidy the wording. Refining uses part of your monthly AI budget.
|
|
553
|
-
|
|
554
|
-
## Find your articles
|
|
555
|
-
|
|
556
|
-
Search matches the title, text and category. **Filters** narrow by type, status and category, and you can sort by most viewed, newest or A to Z. Switch between grid and list views; Kerabie remembers your choice. The preview button shows an article the way customers see it in the widget.
|
|
557
|
-
|
|
558
|
-
## What each card shows
|
|
559
|
-
|
|
560
|
-
| | |
|
|
561
|
-
|---|---|
|
|
562
|
-
| **Views** | How many times customers opened it. |
|
|
563
|
-
| **Helpful** | The share of customers who answered "Was this helpful?" with yes. Shown once at least 3 have voted. |
|
|
564
|
-
| **AI cited** | How many AI answers used the article. |
|
|
565
|
-
|
|
566
|
-
## Archive or delete
|
|
567
|
-
|
|
568
|
-
- **Archive** hides an article from the widget and the AI but keeps it in your account.
|
|
569
|
-
- **Delete** is permanent and removes the article from the widget straight away.
|
|
570
|
-
|
|
571
|
-
Use archive for seasonal content you may bring back, and delete for content that is wrong.`, headings: [{ level: 2, text: "Create an article" }, { level: 2, text: "Find your articles" }, { level: 2, text: "What each card shows" }, { level: 2, text: "Archive or delete" }] }, { id: "knowledge-base/faq-widget", url: "https://kerabie.com/docs/knowledge-base/faq-widget", title: "FAQ Widget", description: "Show knowledge base articles inside the chat widget.", section: "knowledge-base", body: 'The FAQ widget displays your published knowledge base articles directly inside the Kerabie chat bubble, so visitors can find answers before starting a conversation.\n\n## Enabling the FAQ tab\n\n1. Go to **Settings \u2192 Chat Widget \u2192 Features**\n2. Toggle **Show FAQ tab** on\n3. Save\n\nA **FAQ** tab appears next to the **Chat** tab in the widget. Visitors can search and browse articles without leaving your page.\n\n## How articles appear\n\n- Articles are grouped by category\n- The search bar at the top filters articles by title and content\n- Clicking an article opens it in a full-screen view within the widget\n- A **Start chat** button at the bottom lets visitors escalate to a live agent\n\n## Controlling which articles show\n\nOnly **published** articles appear in the FAQ widget. Draft and archived articles are hidden.\n\nTo reorder articles within a category, drag them in the **Knowledge Base** sidebar.\n\n## Customising the FAQ tab label\n\n1. Go to **Settings \u2192 Chat Widget \u2192 Text overrides**\n2. Find **FAQ tab label** and change it (e.g. "Help" or "Articles")\n3. Save\n\n## Search behaviour\n\nThe FAQ search uses keyword matching across:\n- Article title\n- Article body text\n- Category name\n\nIt is not AI-powered \u2014 for AI-based article suggestions in chat, see the [AI Assistant](/docs/ai/setup) docs.\n\n## Analytics\n\nView which articles are being read and searched in **Analytics \u2192 Knowledge Base**. Use this data to identify gaps \u2014 if visitors search for a term with no results, consider writing that article.', headings: [{ level: 2, text: "Enabling the FAQ tab" }, { level: 2, text: "How articles appear" }, { level: 2, text: "Controlling which articles show" }, { level: 2, text: "Customising the FAQ tab label" }, { level: 2, text: "Search behaviour" }, { level: 2, text: "Analytics" }] }, { id: "knowledge-base/import-url", url: "https://kerabie.com/docs/knowledge-base/import-url", title: "Import from URL", description: "Crawl your existing docs or website into the knowledge base, choosing exactly which pages to bring in.", section: "knowledge-base", body: "If you already have a help centre, documentation site, or blog, you can crawl that content into Kerabie's knowledge base instead of rewriting everything from scratch.\n\n## How it works\n\n1. Go to **Knowledge Base \u2192 Crawl a website**\n2. Enter the URL of your existing site\n3. Kerabie discovers pages on that domain (from on-page links, and from `sitemap.xml` when one exists) and shows you the list\n4. Pick exactly which pages you want imported \u2014 nothing is crawled until you choose\n5. AI extracts and structures each selected page's content into draft articles\n6. Review and publish the ones you want\n\nDiscovery is instant and free. The actual crawl runs in the background (it can take a little while for several pages plus AI processing), so you'll see a progress state while it finishes.\n\n## What gets imported\n\n| Content | Imported? |\n|---|---|\n| Page title | Yes \u2192 article title |\n| Body text and headings | Yes \u2192 article content |\n| Images | No \u2014 images aren't imported; add a cover photo to an article afterward if you want one |\n| Navigation, footer, header | No \u2014 stripped automatically before AI sees the page |\n\n## Page limits\n\nThe number of pages you can select in one crawl run depends on your plan:\n\n| Plan | Pages per crawl |\n|---|---|\n| Free | Not available |\n| Basic | 20 |\n| Pro | 100 |\n| Enterprise | Unlimited |\n\nThis is a per-run cap \u2014 a page you don't select in one run can still be picked up in a later one. Crawling also counts against your plan's shared monthly AI-operations budget (import, crawl, and AI refine draw from the same pool).\n\n## After importing\n\nAll imported articles start as **drafts**. Review each one before publishing:\n\n- Remove any leftover navigation text the AI didn't catch\n- Update the category\n- Add a cover photo if you want one \u2014 crawled pages never bring their own images\n- Optionally run **Refine with AI** on an article to tighten the writing further\n\n## Re-crawling\n\nRunning a crawl again on pages you've already imported creates new draft articles rather than updating the existing ones \u2014 archive or delete the old version first if you want a clean replacement.", headings: [{ level: 2, text: "How it works" }, { level: 2, text: "What gets imported" }, { level: 2, text: "Page limits" }, { level: 2, text: "After importing" }, { level: 2, text: "Re-crawling" }] }, { id: "knowledge-base/import", url: "https://kerabie.com/docs/knowledge-base/import", title: "Import and crawl", description: "Bring existing content into the Knowledge Base from files or from your website.", section: "knowledge-base", body: "Imported content always arrives as **drafts**, so nothing reaches customers or the AI until you review and publish it.\n\n## Import a file\n\nChoose **Import** on the Knowledge Base page and pick a file (up to 5 MB).\n\n| File | What happens |\n|---|---|\n| **CSV** | One draft per row. No AI is used, so it doesn't use your AI budget. |\n| **Word (.docx), Markdown, text, PDF, JSON** | AI reads the file and splits it into articles. Headings and lists in a Word or Markdown file are kept. |\n\n### CSV format\n\nUse a header row. Only a title and content are required.\n\n| Column | Notes |\n|---|---|\n| `title` (or `question`) | Required. |\n| `content` (or `answer`) | Required. Plain text becomes paragraphs. |\n| `category` | Optional. Defaults to *Imported*. |\n| `type` | Optional: `article`, `faq` or `news`. Rows with a `question` column default to FAQ. |\n\nUp to 500 rows per file. Rows that can't be used (no title, or no content) are skipped and listed by their row number, so you can fix them and import again.\n\n## Crawl a website\n\nChoose **Crawl URL**, enter the address of your help site, and **Find pages**. Select the pages you want (the first ten are ticked for you) and start the crawl. Kerabie reads each page and creates a draft article from it. Crawling runs in the background and uses your monthly AI budget and the page limit of your plan.\n\n## How imported articles are written\n\nImported and crawled text is rewritten into articles that are easy to read: a short answer first, then a heading for each section, steps as numbered lists, options as bullet lists and key terms in bold. Facts, numbers, names, prices and links are kept exactly as they are in the source, and nothing is invented. A long page becomes several focused articles instead of one wall of text.\n\nEach page you select in a crawl is read in full on its own. If an article from an earlier import reads like plain text, open it and choose **Refine with AI** to restructure it the same way.", headings: [{ level: 2, text: "Import a file" }, { level: 3, text: "CSV format" }, { level: 2, text: "Crawl a website" }, { level: 2, text: "How imported articles are written" }] }, { id: "knowledge-base/overview", url: "https://kerabie.com/docs/knowledge-base/overview", title: "Knowledge Base", description: "Create and manage articles that power the AI assistant and the FAQ widget.", section: "knowledge-base", body: 'The Kerabie Knowledge Base is a collection of articles your team writes. It serves two purposes:\n\n1. **AI training** \u2014 the AI assistant reads your articles to answer visitor questions automatically\n2. **FAQ widget** \u2014 articles appear inside the chat widget so visitors can self-serve before starting a conversation\n\n## Getting started\n\n1. Go to **Knowledge Base** in the sidebar\n2. Click **New article**\n3. Write your article using the rich-text editor\n4. Click **Publish**\n\nThe article is immediately available to the AI and in the FAQ widget.\n\n## How it connects to AI\n\nWhen a visitor asks a question, the AI searches your published articles for relevant content before generating a response. The better your articles, the more accurate the AI answers.\n\nTips for AI-friendly articles:\n- Use plain, clear language \u2014 avoid jargon\n- One topic per article\n- Include the exact phrases visitors might use (e.g. "cancel my subscription", "reset password")\n- Keep articles up to date \u2014 the AI uses whatever is published\n\n## Structure\n\nArticles are organised into **categories**. Create categories to group related articles (e.g. "Billing", "Getting Started", "Technical").\n\nTo create a category:\n1. Click **New category** in the sidebar\n2. Give it a name and optional description\n3. Articles can be moved between categories at any time\n\n## Article visibility\n\n| Status | Visible in FAQ widget | Used by AI |\n|---|---|---|\n| Draft | No | No |\n| Published | Yes | Yes |\n| Archived | No | No |\n\n## Next steps\n\n- [Writing articles](/docs/knowledge-base/articles) \u2014 editor, formatting, and SEO tips\n- [FAQ widget](/docs/knowledge-base/faq-widget) \u2014 show articles inside the chat widget\n- [Import from URL](/docs/knowledge-base/import-url) \u2014 scrape your existing help docs', headings: [{ level: 2, text: "Getting started" }, { level: 2, text: "How it connects to AI" }, { level: 2, text: "Structure" }, { level: 2, text: "Article visibility" }, { level: 2, text: "Next steps" }] }, { id: "notifications/email", url: "https://kerabie.com/docs/notifications/email", title: "Email Notifications", description: "Manage when and how Kerabie sends email alerts to agents.", section: "notifications", body: "Kerabie can send email notifications to agents for important events so nothing slips through even when they're offline.\n\n## Configuring email notifications\n\n1. Go to **Settings \u2192 Notifications \u2192 Email**\n2. Toggle each event on or off\n3. Changes take effect immediately \u2014 no save button needed\n\n## Available email events\n\n| Event | Description |\n|---|---|\n| New conversation assigned | You are assigned a new conversation |\n| New message (assigned) | A visitor replies in a conversation assigned to you |\n| New unassigned conversation | A conversation arrives with no assignee |\n| Conversation reopened | A resolved conversation is reopened |\n| Mentioned in note | An agent @mentions you in an internal note |\n| Daily summary | A digest of open/unresolved conversations (sent at 8 AM) |\n\n## Notification email address\n\nNotification emails are sent from `notifications@kerabie.com`. Add this to your contacts or allowlist if they land in spam.\n\n## Per-agent preferences\n\nEach agent controls their own notification preferences. Admins cannot override individual agent settings, but they can set **organisation-level defaults** that apply to new agents when they join.\n\nSet defaults under **Settings \u2192 Notifications \u2192 Default preferences**.\n\n## Unsubscribing\n\nEvery notification email includes an **Unsubscribe** link at the bottom. Clicking it turns off that specific event for your account. You can re-enable it from Settings at any time.", headings: [{ level: 2, text: "Configuring email notifications" }, { level: 2, text: "Available email events" }, { level: 2, text: "Notification email address" }, { level: 2, text: "Per-agent preferences" }, { level: 2, text: "Unsubscribing" }] }, { id: "notifications/mobile", url: "https://kerabie.com/docs/notifications/mobile", title: "Mobile Notifications", description: "Push notifications for agents on iOS and Android.", section: "notifications", body: "If you use the Kerabie mobile app (via the React Native SDK or the native app), you can receive push notifications directly to your phone.\n\n## Setting up mobile notifications\n\n1. Install the Kerabie app on your device\n2. Sign in with your agent account\n3. When prompted, allow push notification permissions\n4. Go to **Settings \u2192 Notifications \u2192 Mobile** to configure which events trigger a notification\n\n## Notification events\n\n| Event | Default |\n|---|---|\n| New conversation assigned to you | On |\n| New message in your conversations | On |\n| New unassigned conversation | Off |\n| Mention in internal note | On |\n| Conversation reopened | On |\n\n## Sounds and badges\n\nThe app badge (conversation count) updates in real time. Notification sounds use your device's default alert sound. You can silence Kerabie notifications without disabling them by using your device's **Focus** (iOS) or **Do Not Disturb** (Android) mode.\n\n## Troubleshooting\n\n**Not receiving notifications on iOS:**\n- Go to **Settings \u2192 Notifications \u2192 Kerabie** and ensure notifications are set to **Allow**\n- Make sure Background App Refresh is enabled for Kerabie\n\n**Not receiving notifications on Android:**\n- Go to **Settings \u2192 Apps \u2192 Kerabie \u2192 Notifications** and enable all channels\n- Check that battery optimisation is not restricting Kerabie in the background\n\n**Token expired:**\nLog out of the mobile app and log back in to refresh the push notification token.", headings: [{ level: 2, text: "Setting up mobile notifications" }, { level: 2, text: "Notification events" }, { level: 2, text: "Sounds and badges" }, { level: 2, text: "Troubleshooting" }] }, { id: "notifications/push", url: "https://kerabie.com/docs/notifications/push", title: "Push Notifications", description: "Configure browser push notifications for agents.", section: "notifications", body: "Browser push notifications alert agents to new conversations and messages even when they're not looking at the Kerabie tab.\n\n## Enabling push notifications\n\n1. Log in to [kerabie.com/app](https://kerabie.com/app)\n2. Go to **Settings \u2192 Notifications \u2192 Push**\n3. Click **Enable browser notifications**\n4. Allow the browser permission prompt\n\nYou'll start receiving notifications immediately for any of the events you have toggled on.\n\n## What triggers a push notification\n\n| Event | Default |\n|---|---|\n| New conversation assigned to you | On |\n| New message in your assigned conversation | On |\n| New unassigned conversation | Off |\n| Conversation reopened | On |\n| Mention in internal note | On |\n\nToggle each event individually under **Settings \u2192 Notifications**.\n\n## Browser support\n\nPush notifications work in Chrome, Edge, Firefox, and Safari 16.1+. They require HTTPS and a granted permission \u2014 browsers won't prompt twice if you denied the first time. To re-enable:\n\n- **Chrome**: Click the lock icon in the address bar \u2192 Notifications \u2192 Allow\n- **Firefox**: Preferences \u2192 Privacy & Security \u2192 Notifications \u2192 Allow for kerabie.com\n- **Safari**: System Preferences \u2192 Notifications \u2192 Safari \u2192 Allow\n\n## Not receiving notifications?\n\n- Check your OS-level notification settings (macOS / Windows) \u2014 the browser must be allowed there too\n- Make sure your status is set to **Available** in the agent status indicator\n- Ensure the Kerabie tab has been open at least once in the current browser session to register the service worker", headings: [{ level: 2, text: "Enabling push notifications" }, { level: 2, text: "What triggers a push notification" }, { level: 2, text: "Browser support" }, { level: 2, text: "Not receiving notifications?" }] }, { id: "quickstart", url: "https://kerabie.com/docs/quickstart", title: "Quickstart", description: "Add Kerabie to your website and start your first conversation in under 5 minutes.", section: "quickstart", body: '## Step 1 \u2014 Create your account\n\nGo to [kerabie.com/app/register](https://kerabie.com/app/register) and sign up. You\'ll be on a free trial automatically.\n\n## Step 2 \u2014 Copy your widget script\n\nAfter signing up, go to **Settings \u2192 Widget**. Copy the embed script:\n\n```html\n<script>\n window.KERConfig = { widgetKey: "wk_yourkey_here" };\n</script>\n<script async src="https://cdn.kerabie.com/widget.js"></script>\n```\n\n## Step 3 \u2014 Paste it into your site\n\nAdd both script tags before the closing `</body>` tag of every page you want the widget on.\n\n <Tab title="HTML">\n ```html\n <!DOCTYPE html>\n <html>\n <body>\n <!-- your content -->\n\n <script>\n window.KERConfig = { widgetKey: "wk_yourkey_here" };\n </script>\n <script async src="https://cdn.kerabie.com/widget.js"></script>\n </body>\n </html>\n ```\n </Tab>\n <Tab title="Next.js">\n ```tsx\n // app/layout.tsx\n import Script from \'next/script\';\n\n export default function RootLayout({ children }) {\n return (\n <html>\n <body>\n {children}\n <Script id="kerabie-settings" strategy="beforeInteractive">\n {`window.KERConfig = { widgetKey: "wk_yourkey_here" };`}\n </Script>\n <Script src="https://cdn.kerabie.com/widget.js" strategy="afterInteractive" />\n </body>\n </html>\n );\n }\n ```\n </Tab>\n <Tab title="WordPress">\n Install the [Kerabie WordPress plugin](/integrations/wordpress) \u2014 no code needed.\n </Tab>\n\n## Step 4 \u2014 Test it\n\nOpen your site. The chat bubble appears in the bottom-right corner. Click it and send a message.\n\nIn the Kerabie dashboard, go to **Conversations** \u2014 you\'ll see your test message arrive in real time.\n\n## Step 5 \u2014 Invite your team (optional)\n\nGo to **Settings \u2192 Agents \u2192 Invite agent**. Enter their email \u2014 they\'ll get a link to join your workspace.\n\n---\n\n You\'re live. Visitors can now start conversations and your agents can reply from the dashboard or mobile app.\n\n## What\'s next?\n\n <Card title="Customize the widget" icon="palette" href="/widget/customization">\n Change colors, position, welcome message, and logo.\n </Card>\n <Card title="Connect AI" icon="robot" href="/ai/setup">\n Let AI handle common questions automatically.\n </Card>\n <Card title="Set business hours" icon="clock" href="/widget/offline-mode">\n Show an offline form when no agents are available.\n </Card>\n <Card title="Install on WordPress" icon="wordpress" href="/integrations/wordpress">\n One-click plugin install, no code required.\n </Card>', headings: [{ level: 2, text: "Step 1 \u2014 Create your account" }, { level: 2, text: "Step 2 \u2014 Copy your widget script" }, { level: 2, text: "Step 3 \u2014 Paste it into your site" }, { level: 2, text: "Step 4 \u2014 Test it" }, { level: 2, text: "Step 5 \u2014 Invite your team (optional)" }, { level: 2, text: "What's next?" }] }, { id: "sdks/mcp", url: "https://kerabie.com/docs/sdks/mcp", title: "MCP server", description: "Let Claude, Cursor and other AI assistants build on Kerabie and work with your account.", section: "sdks", body: 'Kerabie has a [Model Context Protocol](https://modelcontextprotocol.io) server. It does two jobs.\n\n- **Build with Kerabie.** Your coding assistant can search the docs, list API endpoints, read the TypeScript and React Native SDK types, and follow integration guides, so it writes correct code for the [API](/docs/api-reference/overview), the [TypeScript SDK](/docs/sdks/typescript), the [React Native SDK](/docs/sdks/react-native), the web widget and WordPress. This works **without an API key**.\n- **Work with your account.** With an API key, the same assistant can use your live data. Connect it to Claude Desktop, Claude Code, Cursor or any MCP client and you can ask things like *"which tickets are about to breach SLA?"*, *"draft a Knowledge Base article from this conversation"* or *"pause the pricing-page campaign"*.\n\nThe account tools are a thin layer over the API and `@kerabie/sdk`, so they do exactly what your API key is allowed to do, and nothing else. The developer tools answer from documentation bundled with the package, so they match the SDK version you install.\n\n## Hosted server (no install)\n\nPoint your client at the hosted endpoint and send your API key as a Bearer token. Nothing runs on your machine.\n\n```\nhttps://mcp.kerabie.com/mcp\nAuthorization: Bearer ker_live_...\n```\n\n**Claude Code:**\n\n```bash\nclaude mcp add --transport http kerabie https://mcp.kerabie.com/mcp --header "Authorization: Bearer ker_live_..."\n```\n\n**Cursor** (`.cursor/mcp.json`):\n\n```json\n{\n "mcpServers": {\n "kerabie": {\n "url": "https://mcp.kerabie.com/mcp",\n "headers": { "Authorization": "Bearer ker_live_..." }\n }\n }\n}\n```\n\nAdd `?readOnly=true` to the URL to leave out every tool that changes data. Without a key you get the developer tools only. Your key is used for every call and is never stored, so the server can do exactly what your key allows.\n\n## Get an API key\n\nThe developer tools work without a key. To let your assistant work with your account, create an API key under **Settings \u2192 Integrations \u2192 API Keys** (API access is on the Pro plan and above) and send it as the Bearer token shown above.\n\n> **Careful:** an MCP client can use every tool the key allows, including ones that reply to customers or delete things. Start with `?readOnly=true` on the URL, and keep the client\'s approval prompts on for anything that changes data.\n\n## Developer tools\n\nAvailable with or without an API key. None of them change anything.\n\n| Tool | What it does |\n|---|---|\n| `search_docs` | Search the docs, SDK readmes and API reference. |\n| `get_doc` | Read a docs page, or one section of it. |\n| `list_docs` | Every page with its description, by section. |\n| `list_api_endpoints` | Every REST endpoint with its docs page and the equivalent TypeScript SDK call. |\n| `get_sdk_types` | The TypeScript or React Native SDK type declarations: list them, or fetch the ones you name. |\n| `get_integration_guide` | The pages to follow for `typescript`, `react-native`, `wordpress`, `web-widget`, `rest-api` or `mcp`. |\n\nThe server also offers ready-made **prompts** (`integrate_react_native`, `integrate_typescript_sdk`, `integrate_rest_api`, `install_web_widget`) and every docs page as a **resource** (`kerabie-docs://sdks/react-native`) for clients that support them.\n\n## Account tools\n\nThese need `KERABIE_API_KEY`.\n\nTools that change something are marked **write**; those that delete are also marked **destructive**, so clients can ask before running them.\n\n| Area | Tools |\n|---|---|\n| **Conversations** | `list_conversations`, `get_conversation`, `get_customer_history`, `take_over_conversation` *(write)* |\n| **Tickets** | `list_tickets`, `get_ticket_stats`, `get_ticket`, `create_ticket` *(write)*, `update_ticket` *(write)*, `reply_to_ticket` *(write)* |\n| **Contacts** | `list_contacts`, `get_contact_stats`, `get_contact`, `create_contact` *(write)*, `update_contact` *(write)* |\n| **Knowledge Base** | `search_knowledge_base`, `get_kb_stats`, `get_kb_insights`, `get_article`, `create_article` *(write)*, `update_article` *(write)*, `delete_article` *(destructive)* |\n| **Campaigns** | `list_campaigns`, `get_campaign_stats`, `get_campaign`, `create_campaign`, `update_campaign`, `duplicate_campaign` *(write)*, `delete_campaign` *(destructive)* |\n| **Analytics** | `get_analytics_summary`, `get_analytics_insights`, `get_agent_stats`, `get_ticket_sla`, `get_setup_status` |\n| **Visitors and store** | `list_visitors`, `get_visitors_overview`, `list_store_products` |\n| **AI tools** | `list_ai_tools`, `get_ai_tool`, `create_ai_tool`, `update_ai_tool`, `duplicate_ai_tool` *(write)*, `delete_ai_tool` *(destructive)*, `test_ai_tool` *(write: calls your endpoint)* |\n| **MCP servers** | `list_mcp_servers`, `connect_mcp_server` *(write)*, `sync_mcp_server` *(write)*, `update_mcp_server` *(write)*, `test_mcp_server_tool` *(write)*, `disconnect_mcp_server` *(destructive)* |\n\nLong results are cut at 60,000 characters with a note to use `page`, `limit` or filters. API errors come back as readable messages, for example `Kerabie API error 403: Your plan includes 1 AI agent tool. Upgrade to add more.`\n\n## Use MCP servers from your chatbot\n\nThe other direction: your **AI chatbot** can use tools from any remote MCP server. See [Connecting MCP servers](/docs/ai/mcp-servers).', headings: [{ level: 2, text: "Hosted server (no install)" }, { level: 2, text: "Get an API key" }, { level: 2, text: "Developer tools" }, { level: 2, text: "Account tools" }, { level: 2, text: "Use MCP servers from your chatbot" }] }, { id: "sdks/react-native", url: "https://kerabie.com/docs/sdks/react-native", title: "React Native SDK", description: "Add Kerabie live chat to your iOS or Android app.", section: "sdks", body: "The `react-native-kerabie-helpdesk` package adds a fully customizable chat UI to any React Native app (Expo managed or bare workflow).\n\n## Install\n\n```bash\n# Expo\nnpx expo install react-native-kerabie-helpdesk socket.io-client @react-native-async-storage/async-storage\n\n# Bare React Native\nnpm install react-native-kerabie-helpdesk socket.io-client @react-native-async-storage/async-storage\nnpx pod-install\n```\n\n## Quick start\n\nWrap your app in `KerAbieProvider`. Place it below your gesture/safe-area providers:\n\n```tsx\n\nexport default function App() {\n return (\n <GestureHandlerRootView style={{ flex: 1 }}>\n <SafeAreaProvider>\n <KerAbieProvider\n widgetKey=\"wk_yourorg_123\"\n appName=\"My App\"\n appVersion=\"1.0.0\"\n user={{ name: 'Jane', email: 'jane@example.com' }}\n theme={{ primaryColor: '#6d28d9' }}\n >\n <YourNavigator />\n <KerAbieFloatingButton />\n </KerAbieProvider>\n </SafeAreaProvider>\n </GestureHandlerRootView>\n );\n}\n```\n\n## Provider props\n\n| Prop | Type | Required | Description |\n|---|---|---|---|\n| `widgetKey` | `string` | \u2713 | Your widget key from Settings |\n| `appName` | `string` | \u2713 | Human-readable app name shown in dashboard |\n| `appVersion` | `string` | \u2713 | Your app version |\n| `user` | `KerUser` | \u2014 | Pre-identified user (name, email, userId) |\n| `theme` | `KerTheme` | \u2014 | Colors, font family, bubble size |\n| `openMode` | `'modal' \\| 'screen'` | \u2014 | How chat opens (default: `modal`) |\n\n `appName` and `appVersion` are shown to agents in the conversation, so they immediately know which app and version the user is on. Critical for bug reports and version-specific support.\n\n## Hooks\n\n```typescript\n\nconst { open, close, toggle, sendMessage, unreadCount, isConnected } = useKerAbieContext();\n\n// Open chat\n\n <Text>Chat with us</Text>\n {unreadCount > 0 && <Badge count={unreadCount} />}\n\n```\n\n## Campaigns\n\nProactive messages you create in the dashboard (**Campaigns**) also appear in your app, above the launcher, with their button. The provider loads your live campaigns once per app launch; there is nothing to add to your code beyond the optional props below.\n\n| Dashboard setting | In the app |\n| ---- | ---- |\n| Trigger **Page load** | Shown about 1.5 seconds after a screen appears (each time the `screen` prop changes). |\n| Trigger **Time on page** | Shown after N seconds in the app. |\n| Trigger **Scroll depth**, **Exit intent** | Website-only. These campaigns are not shown in apps. |\n| **On pages** | Matched against the `screen` prop (for example Expo Router's `usePathname()`). Without the prop, only campaigns for every screen show. |\n| **Audience** | New / returning means the first / a later app launch on this device; \"hasn't chatted\" checks this device's conversations; country is decided by the server from the device's IP. |\n| **Frequency** | Remembered on the device: once per visitor, once per day, or once per app launch. |\n| Button **Open chat** / **Open a link** | Opens the chat / the link in the browser. |\n| Button **Open Knowledge Base** | Calls your `onOpenKnowledgeBase` (the chat has no built-in Knowledge Base screen); opens the chat if you don't pass it. |\n\n```tsx\n\n<KerAbieProvider\n widgetKey=\"wk_yourorg_xxx\"\n screen={usePathname()}\n onOpenKnowledgeBase={() => router.push('/help')}\n>\n```\n\nCampaign results (shown, opened, clicked, replied) are counted in the dashboard like they are for the website widget. Nothing pops up while the chat is open.\n\n## Push notifications\n\n### Android \u2014 register notification channels first\n\nOn **Android 8+ (API 26+)**, push notifications require a registered channel. Without one, notifications are silently dropped. Call `NotificationService.setup()` **before** the provider renders:\n\n```tsx\n\n// In App.tsx / _layout.tsx, before KerAbieProvider:\nuseEffect(() => {\n NotificationService.setup({\n projectId: 'your-expo-project-id', // from app.json / expo.dev\n onTokenReceived: (token) => console.log('Push token:', token),\n });\n}, []);\n```\n\nThis single call:\n1. Registers Android notification channels (`kerabie_messages`, `kerabie_visitor`, `kerabie_system`)\n2. Requests permission from the user\n3. Gets the Expo push token\n4. Registers the token with the Kerabie API automatically\n\n### Custom channels\n\nOverride the default channels to match your app's branding:\n\n```tsx\nawait NotificationService.setup({\n channels: [\n {\n channelId: 'my_app_chat',\n name: 'Support Chat',\n importance: 'high',\n sound: 'default',\n enableVibrate: true,\n },\n ],\n});\n```\n\n### app.json \u2014 required Android config\n\nMake sure your `app.json` includes:\n\n```json\n{\n \"expo\": {\n \"android\": {\n \"permissions\": [\"NOTIFICATIONS\"],\n \"googleServicesFile\": \"./google-services.json\"\n },\n \"plugins\": [\"expo-notifications\"]\n }\n}\n```\n\n### Foreground notifications\n\n```tsx\n\nfunction AppInner() {\n const { requestNotificationPermission } = useKerAbieNotifications();\n\n useEffect(() => {\n requestNotificationPermission();\n }, []);\n\n return <YourApp />;\n}\n```\n\n## Open modes\n\n| Mode | Behavior |\n|---|---|\n| `modal` | Slides up as a bottom sheet over your app |\n| `screen` | Pushes a full screen onto your navigation stack |\n\n```tsx\n\n```\n\n## Contextual chat \u2014 open with context\n\nUse `openWithContext` to open the chat widget with a pre-loaded context payload. The agent sees the context (order details, product info, transaction data) immediately when they open the conversation.\n\nThis is the recommended pattern for any app where users need help about a specific item.\n\n### Open when a product is clicked\n\n```tsx\n\nfunction ProductCard({ product }) {\n const { openWithContext } = useKerAbieContext();\n\n return (\n <Pressable onPress={() => openWithContext({\n type: 'product',\n id: String(product.id),\n title: product.name,\n data: { price: product.price, category: product.category },\n })}>\n <Text>{product.name}</Text>\n </Pressable>\n );\n}\n```\n\n### Open for an order inquiry\n\n```tsx\nconst { openWithContext } = useKerAbieContext();\n\n// In an order detail screen:\n<Button onPress={() => openWithContext({\n type: 'order',\n id: order.reference,\n title: `Order ${order.reference}`,\n data: {\n status: order.status,\n total: order.amount,\n placedAt: order.createdAt,\n },\n})}>\n Get help with this order\n\n```\n\n### Open for a fintech transaction\n\n```tsx\n<Button onPress={() => openWithContext({\n type: 'transaction',\n id: txn.id,\n title: `\u20A6${txn.amount} Transfer`,\n data: { status: txn.status, recipient: txn.recipientName, narration: txn.narration },\n})}>\n Report this transaction\n\n```\n\n### Open for a ride\n\n```tsx\n<Button onPress={() => openWithContext({\n type: 'ride',\n id: ride.id,\n title: `Trip to ${ride.destination}`,\n data: { driver: ride.driverName, fare: ride.fare, status: ride.status },\n})}>\n Get help with this trip\n\n```\n\n### Context type reference\n\n| `type` | Use case |\n|---|---|\n| `product` | E-commerce \u2014 user asking about an item |\n| `order` | Order inquiry or return |\n| `transaction` | Fintech \u2014 failed/disputed payment |\n| `ride` | Ride-sharing \u2014 trip issue |\n| `errand` | Delivery/logistics |\n| `booking` | Appointment or reservation |\n| `custom` | Any other context (pass a descriptive `title`) |\n\n---\n\n## Theming\n\n```tsx\n\n```\n\n## Use with an AI assistant\n\nConnect the [Kerabie MCP server](/docs/sdks/mcp) to Claude, Cursor or another MCP client and it can search these docs, read the React Native SDK types and follow the integration guide while it writes code for you. No API key is needed for that.", headings: [{ level: 2, text: "Install" }, { level: 2, text: "Quick start" }, { level: 2, text: "Provider props" }, { level: 2, text: "Hooks" }, { level: 2, text: "Campaigns" }, { level: 2, text: "Push notifications" }, { level: 3, text: "Android \u2014 register notification channels first" }, { level: 3, text: "Custom channels" }, { level: 3, text: "app.json \u2014 required Android config" }, { level: 3, text: "Foreground notifications" }, { level: 2, text: "Open modes" }, { level: 2, text: "Contextual chat \u2014 open with context" }, { level: 3, text: "Open when a product is clicked" }, { level: 3, text: "Open for an order inquiry" }, { level: 3, text: "Open for a fintech transaction" }, { level: 3, text: "Open for a ride" }, { level: 3, text: "Context type reference" }, { level: 2, text: "Theming" }, { level: 2, text: "Use with an AI assistant" }] }, { id: "sdks/typescript", url: "https://kerabie.com/docs/sdks/typescript", title: "TypeScript / Node.js SDK", description: "Build server-side integrations with the Kerabie TypeScript SDK.", section: "sdks", body: "The `@kerabie/sdk` package lets you integrate Kerabie into your backend, server-side scripts, or custom tooling.\n\n## Install\n\n```bash\nnpm install @kerabie/sdk\n```\n\n## Initialize\n\n```typescript\n\nconst kerabie = new KerAbieClient({\n apiKey: process.env.KERABIE_API_KEY!, // Settings \u2192 API \u2192 New API Key\n orgId: 42, // Your org ID\n // baseUrl: 'https://api.kerabie.com', // default\n});\n```\n\n## Conversations\n\n```typescript\n// List open conversations\nconst { conversations, total } = await kerabie.conversations.list({\n status: 'open',\n page: 1,\n limit: 20,\n});\n\n// Get a conversation\nconst conv = await kerabie.conversations.get(conversationId);\n\n// Resolve\nawait kerabie.conversations.resolve(conversationId);\n\n// Assign to agent\nawait kerabie.conversations.assign(conversationId, { agentId: 7 });\n\n// Send a message as a bot/agent\nawait kerabie.conversations.sendMessage(conversationId, {\n body: 'Your order has been shipped!',\n senderType: 'bot',\n});\n```\n\n## Customers\n\n```typescript\n// Look up by email\nconst customer = await kerabie.customers.getByEmail('jane@example.com');\n\n// List all customers\nconst { customers } = await kerabie.customers.list({ page: 1 });\n\n// Update attributes\nawait kerabie.customers.update(customerId, {\n name: 'Jane Smith',\n attributes: { plan: 'pro', company: 'Acme' },\n});\n\n// Delete (GDPR)\nawait kerabie.customers.delete(customerId);\n```\n\n## Knowledge Base\n\n```typescript\n// Search articles\nconst results = await kerabie.kb.search('refund policy');\n\n// List all articles\nconst { articles } = await kerabie.kb.list({ category: 'billing' });\n\n// Get one article\nconst article = await kerabie.kb.get('how-to-cancel');\n```\n\n## Analytics\n\n```typescript\n// Summary for a date range\nconst summary = await kerabie.analytics.summary({\n from: '2026-05-01',\n to: '2026-05-26',\n});\n// \u2192 { conversations, avgResponseTime, resolvedRate, satisfactionScore }\n\n// Agent performance\nconst agentStats = await kerabie.analytics.agents({ from: '2026-05-01' });\n```\n\n## Webhooks\n\n```typescript\n// Verify an incoming webhook (use with express.raw middleware)\nconst isValid = KerAbieClient.webhooks.verify(\n req.body, // raw Buffer\n req.headers['x-kerabie-signature'],\n process.env.KERABIE_WEBHOOK_SECRET!,\n);\n\n// Parse the event\nconst event = KerAbieClient.webhooks.parse(req.body);\n// \u2192 { event: 'conversation.created', orgId: 42, data: { ... } }\n```\n\n## Resources reference\n\n| Resource | Methods |\n|---|---|\n| `conversations` | `list`, `get`, `resolve`, `close`, `assign`, `sendMessage` |\n| `customers` | `list`, `get`, `getByEmail`, `update`, `delete` |\n| `visitors` | `list`, `get`, `block` |\n| `agents` | `list`, `get` |\n| `kb` | `list`, `search`, `get` |\n| `analytics` | `summary`, `agents` |\n| `webhooks` | `verify` (static), `parse` (static) |\n\n## Error handling\n\nAll methods throw `KerAbieError` on failure:\n\n```typescript\n\ntry {\n await kerabie.conversations.get(99999);\n} catch (err) {\n if (err instanceof KerAbieError) {\n console.log(err.status); // 404\n console.log(err.message); // \"Conversation not found\"\n }\n}\n```\n\n## Use with an AI assistant\n\nConnect the [Kerabie MCP server](/docs/sdks/mcp) to Claude, Cursor or another MCP client and it can search these docs, read the TypeScript SDK types and follow the integration guide while it writes code for you. No API key is needed for that.", headings: [{ level: 2, text: "Install" }, { level: 2, text: "Initialize" }, { level: 2, text: "Conversations" }, { level: 2, text: "Customers" }, { level: 2, text: "Knowledge Base" }, { level: 2, text: "Analytics" }, { level: 2, text: "Webhooks" }, { level: 2, text: "Resources reference" }, { level: 2, text: "Error handling" }, { level: 2, text: "Use with an AI assistant" }] }, { id: "sdks/wordpress", url: "https://kerabie.com/docs/sdks/wordpress", title: "WordPress Plugin", description: "Full reference for the Kerabie WordPress plugin.", section: "sdks", body: "The Kerabie WordPress plugin is the easiest way to add live chat to a WordPress site. It installs the widget automatically on every page without touching any code.\n\nFor the basic installation walkthrough, see [Integrations \u2192 WordPress](/docs/integrations/wordpress).\n\n## Plugin settings reference\n\nAfter activation, go to **WP Admin \u2192 Kerabie \u2192 Settings** to configure:\n\n### General\n\n| Setting | Description |\n|---|---|\n| **Widget Key** | Your Kerabie widget key (`wk_...`) \u2014 required |\n| **Enable on all pages** | Toggle the widget on every page of your site |\n| **Enable on specific pages** | Enter comma-separated page IDs to restrict to those pages |\n| **Hide for logged-out users** | Only show the widget to logged-in WordPress users |\n| **Hide for admins** | Suppress the widget in WP Admin screens |\n\n### User identity\n\n| Setting | Description |\n|---|---|\n| **Pass WordPress user data** | When on, logged-in users' name and email are sent to Kerabie automatically |\n| **Pass user ID** | Also sends the WordPress user ID as `userId` |\n| **Custom user meta** | Pass additional user meta fields as custom attributes (comma-separated meta keys) |\n\n### Widget appearance\n\nThe widget's colours, greeting, and position are controlled from your Kerabie dashboard under **Settings \u2192 Chat Widget** \u2014 not from the WordPress plugin. Changes there reflect immediately on your site without a plugin update.\n\n## Hooks and filters\n\nThe plugin provides WordPress hooks for developers who need custom control:\n\n### Filter: `kerabie_widget_settings`\n\nModify the `window.KERConfig` object before it's output:\n\n```php\nadd_filter('kerabie_widget_settings', function($settings) {\n if (is_user_logged_in()) {\n $user = wp_get_current_user();\n $settings['user']['plan'] = get_user_meta($user->ID, 'subscription_plan', true);\n }\n return $settings;\n});\n```\n\n### Action: `kerabie_before_widget_script`\n\nRun code before the widget script tag:\n\n```php\nadd_action('kerabie_before_widget_script', function() {\n // e.g. output a nonce or inline config\n});\n```\n\n## WooCommerce integration\n\nIf WooCommerce is active, the plugin automatically passes the customer's order count and total spend as custom attributes:\n\n```js\nuser: {\n orders_count: 5,\n total_spend: \"349.00\"\n}\n```\n\nThese appear in the conversation sidebar and can be used in automation conditions.\n\n## Updating the plugin\n\nUpdates are delivered through the standard WordPress plugin updater. You'll see an update notification in **WP Admin \u2192 Plugins** when a new version is available. Always update \u2014 older versions may not receive new widget features.\n\n## Compatibility\n\n- WordPress 5.8+\n- PHP 7.4+\n- Compatible with all major page builders (Elementor, Divi, Oxygen, Bricks)\n- Compatible with WP Rocket, W3 Total Cache, LiteSpeed Cache \u2014 add `cdn.kerabie.com` to each cache plugin's script exclusion list", headings: [{ level: 2, text: "Plugin settings reference" }, { level: 3, text: "General" }, { level: 3, text: "User identity" }, { level: 3, text: "Widget appearance" }, { level: 2, text: "Hooks and filters" }, { level: 3, text: "Filter: `kerabie_widget_settings`" }, { level: 3, text: "Action: `kerabie_before_widget_script`" }, { level: 2, text: "WooCommerce integration" }, { level: 2, text: "Updating the plugin" }, { level: 2, text: "Compatibility" }] }, { id: "test", url: "https://kerabie.com/docs/test", title: "Components", description: "Components", section: "test", body: '## Code Block\n\n```js\nconsole.log(\'Hello World\');\n```\n\n## Cards\n\n <Card title="Learn more about Next.js" href="https://nextjs.org/docs" />\n <Card title="Learn more about Fumadocs" href="https://fumadocs.dev" />', headings: [{ level: 2, text: "Code Block" }, { level: 2, text: "Cards" }] }, { id: "visitors/blocking", url: "https://kerabie.com/docs/visitors/blocking", title: "Blocking Visitors", description: "Block abusive or spam visitors from using your chat widget.", section: "visitors", body: "If a visitor is sending spam, abusive messages, or repeatedly opening conversations to harass your team, you can block them. Blocked visitors still see the widget but cannot send messages.\n\n## Blocking a visitor\n\n**From a conversation:**\n\n1. Open the conversation with the visitor\n2. Click the **\u22EF more options** menu in the top-right\n3. Click **Block visitor**\n4. Confirm\n\n**From the Visitors list:**\n\n1. Click the visitor in the **Visitors** list\n2. Click **Block**\n\nThe block takes effect immediately.\n\n## What a blocked visitor experiences\n\nThe widget loads normally but when they try to send a message, they see:\n\n> \"You are unable to start a conversation at this time.\"\n\nThere is no indication that they've been specifically blocked.\n\n## Blocking by IP address\n\nKerabie blocks at the **visitor ID** level (session cookie) by default. For persistent spam from a known IP address:\n\n1. Go to **Settings \u2192 Chat Widget \u2192 Blocked IPs**\n2. Enter the IP address or CIDR range (e.g. `192.168.1.0/24`)\n3. Save\n\nIP blocks apply across all visitors from that address.\n\n## Viewing and unblocking\n\nGo to **Settings \u2192 Chat Widget \u2192 Blocked visitors** to see the full list. Click **Unblock** next to any entry to restore access.\n\n## Limitations\n\n- Blocking by visitor ID only works if the visitor uses the same browser \u2014 clearing cookies lets them start fresh with a new session ID\n- IP blocking is more persistent but affects all visitors from that IP (e.g. office networks or shared IPs)\n- Blocking does not delete past conversations \u2014 those remain in history\n\n## Reporting\n\nIf a visitor sends threatening or illegal content, block them and document the conversation before it's needed for reporting. Use **Export \u2192 CSV** on the conversation to save a record.", headings: [{ level: 2, text: "Blocking a visitor" }, { level: 2, text: "What a blocked visitor experiences" }, { level: 2, text: "Blocking by IP address" }, { level: 2, text: "Viewing and unblocking" }, { level: 2, text: "Limitations" }, { level: 2, text: "Reporting" }] }, { id: "visitors/overview", url: "https://kerabie.com/docs/visitors/overview", title: "Visitors", description: "See who is on your site in real time and start conversations proactively.", section: "visitors", body: `The **Visitors** view shows every person currently on your site with the Kerabie widget loaded \u2014 in real time. You can see what page they're on, how long they've been there, and start a conversation with them before they ask.
|
|
572
|
-
|
|
573
|
-
## Accessing the Visitors view
|
|
574
|
-
|
|
575
|
-
Click **Visitors** in the sidebar. The list updates live \u2014 new visitors appear as they land and drop off as they leave.
|
|
576
|
-
|
|
577
|
-
## What you can see per visitor
|
|
578
|
-
|
|
579
|
-
| Field | Description |
|
|
580
|
-
|---|---|
|
|
581
|
-
| **Page** | The current page URL the visitor is on |
|
|
582
|
-
| **Time on site** | How long they've been on your site this session |
|
|
583
|
-
| **Location** | Country and city (from IP geolocation) |
|
|
584
|
-
| **Device** | Browser and operating system |
|
|
585
|
-
| **Visit count** | Total number of times they've visited |
|
|
586
|
-
| **Last seen** | When they were last on your site |
|
|
587
|
-
| **Name / Email** | If they've identified themselves or started a conversation before |
|
|
588
|
-
|
|
589
|
-
## Starting a conversation
|
|
590
|
-
|
|
591
|
-
Click any visitor in the list, then click **Start conversation**. A message appears in the widget on their screen \u2014 exactly like a proactive message. If they reply, it becomes a live conversation in your inbox.
|
|
592
|
-
|
|
593
|
-
## Identified vs anonymous visitors
|
|
594
|
-
|
|
595
|
-
- **Anonymous** \u2014 visitor has not provided their name or email yet; shown as "Visitor #1234"
|
|
596
|
-
- **Identified** \u2014 visitor has sent a message, filled in the pre-chat form, or been identified via the widget SDK
|
|
597
|
-
|
|
598
|
-
Once a visitor is identified, all their sessions are linked and shown in their **Contact** profile.
|
|
599
|
-
|
|
600
|
-
## Filtering the list
|
|
601
|
-
|
|
602
|
-
Filter visitors by:
|
|
603
|
-
- Current page URL
|
|
604
|
-
- Country
|
|
605
|
-
- Device type
|
|
606
|
-
- Visit count (e.g. returning visitors only)
|
|
607
|
-
- Has/has not started a conversation
|
|
608
|
-
|
|
609
|
-
## Related
|
|
610
|
-
|
|
611
|
-
- [Visitor Tracking](/docs/visitors/tracking) \u2014 what data is collected and how
|
|
612
|
-
- [Blocking Visitors](/docs/visitors/blocking) \u2014 block spam or abusive visitors
|
|
613
|
-
- [Proactive Messages](/docs/campaigns/proactive) \u2014 automatically message visitors based on page and time`, headings: [{ level: 2, text: "Accessing the Visitors view" }, { level: 2, text: "What you can see per visitor" }, { level: 2, text: "Starting a conversation" }, { level: 2, text: "Identified vs anonymous visitors" }, { level: 2, text: "Filtering the list" }, { level: 2, text: "Related" }] }, { id: "visitors/tracking", url: "https://kerabie.com/docs/visitors/tracking", title: "Visitor Tracking", description: "What data Kerabie collects about visitors and how to use it.", section: "visitors", body: 'Kerabie collects behavioural data about each visitor session to help your team provide faster, more contextual support. No third-party tracking pixels are used \u2014 all data flows directly to Kerabie.\n\n## What is collected automatically\n\n| Data point | How it\'s collected |\n|---|---|\n| Page URL and title | Captured on each page load |\n| Time on page / site | Tracked via JavaScript timer |\n| Referrer | HTTP referrer header from the first page load |\n| Country, city | Derived from IP address (IP is not stored) |\n| Browser and OS | User-Agent header |\n| Device type | Desktop / tablet / mobile |\n| Session ID | Anonymous cookie set in the visitor\'s browser |\n| Visit count | Incremented per session using the session cookie |\n\n## What is NOT collected by default\n\n- Name and email \u2014 only captured if the visitor starts a conversation or fills a pre-chat form\n- Any data from other tabs or websites\n- Keystrokes outside the chat widget\n\n## Identifying visitors\n\nPass known user data from your app to Kerabie using the widget SDK:\n\n```html\n<script>\n window.KERConfig = {\n widgetKey: "wk_yourkey_here",\n user: {\n name: "Jane Smith",\n email: "jane@example.com",\n userId: "user_123",\n plan: "pro" // custom attribute\n }\n };\n</script>\n```\n\nAny fields you pass under `user` are stored as visitor attributes and visible in the conversation sidebar and Visitors list.\n\n## Custom attributes\n\nYou can pass any key/value pair under `user` \u2014 not just name and email. Custom attributes appear in:\n- The visitor profile panel during conversations\n- The Visitors list (filterable)\n- Automation conditions (e.g. `plan = enterprise`)\n\n## Data retention\n\nVisitor session data is retained for **12 months** on all paid plans, **30 days** on Free.\n\n## GDPR considerations\n\n- Kerabie processes visitor data as a **data processor** on your behalf \u2014 you are the data controller\n- The session cookie (`kw_session`) is strictly necessary for the widget to function \u2014 it does not require explicit consent under ePrivacy rules in most jurisdictions, but consult your legal team\n- Visitors can request deletion via your [Contacts](/docs/conversations/history-export) panel or by submitting a GDPR request to you directly', headings: [{ level: 2, text: "What is collected automatically" }, { level: 2, text: "What is NOT collected by default" }, { level: 2, text: "Identifying visitors" }, { level: 2, text: "Custom attributes" }, { level: 2, text: "Data retention" }, { level: 2, text: "GDPR considerations" }] }, { id: "widget/customization", url: "https://kerabie.com/docs/widget/customization", title: "Widget Customization", description: "Change colors, logo, layout, messages, and behavior to match your brand.", section: "widget", body: 'All widget settings live in **Settings \u2192 Widget Settings** in the dashboard. Click **Save Changes** and they apply to your live widget and the React Native SDK \u2014 no re-deploy needed.\n\nClick **Preview** at the top of that page to open your saved widget on a sample web page in a new tab.\n\n## Style\n\n- **Colors** \u2014 by default the widget uses a slate \u2192 teal gradient (`#334155` \u2192 `#0f766e`). Pick your own brand color from the presets or the color picker, and switch between **Solid** and **Gradient**; the launcher button, header, and visitor messages use it. **Reset** puts the colors back to the default.\n- **Logo** \u2014 upload an image (PNG, JPG, GIF, or WebP, up to 5 MB); free on every plan. It appears in the widget header and welcome bubble.\n- **Position** \u2014 bottom right, bottom left, top right, or top left.\n- **Theme** \u2014 light, dark, or auto (follows the visitor\'s OS preference).\n- **Border radius** \u2014 none, small, medium, or large corner rounding for the chat window and the launcher button.\n- **"Powered by Kerabie"** \u2014 a small credit under the message box. Hiding it is included on every paid plan, or available as an add-on on Free.\n\n## Launcher and layout\n\n- **Launcher** \u2014 a floating **chat bubble**, or a wide **search bar** ("Ask anything\u2026") docked to the top or bottom of the page. Typing in the bar and pressing Enter opens the chat and sends what was typed. The search bar has no attachment button \u2014 attach files from the chat itself once it opens.\n- **Bubble icon** \u2014 upload your own icon for the bubble (Pro and Enterprise), and set the **icon size** (16\u201348 px) and **label font size** (10\u201320 px).\n- **Chat layout** \u2014 **Full widget** (Home, conversations, help and tickets tabs) or **Single chat**: just the chat, with all of a visitor\'s messages shown as one continuous conversation. In Single chat you can optionally turn on the **bottom navigation** and a header **menu** (change name, sounds, download or email transcript).\n\nYour settings are remembered in the visitor\'s browser, so the widget shows the right look immediately on every page load and updates quietly if you have changed anything since.\n\n## AI page awareness\n\nOn every paid plan, turn on **AI page awareness** (Settings > Chat > AI). The AI assistant can then answer questions about the page the visitor is currently on \u2014 "what does this page say about pricing?" \u2014 and can look at screenshots they attach or paste into the chat. While it works, visitors see an animated "thinking" orb.\n\n## Messages\n\n- **Welcome message** \u2014 the greeting shown before a visitor starts chatting. Add `{{name}}` to greet the visitor by first name (it is dropped when their name is unknown).\n- **When the chat bubble / greeting is clicked** \u2014 choose what opens: the home screen, a new conversation, help articles, or tickets (when tickets are on).\n- **Tickets** \u2014 the Tickets tab is off by default; turn it on under Behaviour.\n- **Offline message** \u2014 shown when your team is offline. See [Offline Mode](/docs/widget/offline-mode).\n- **Quick replies** \u2014 suggestion chips shown before the visitor\'s first message.\n\n## Behavior\n\n- **Show typing indicator** \u2014 visitors see when an agent is typing.\n- **Sound notifications** \u2014 play a sound in the widget when a new agent or bot reply arrives.\n- **Save chat history** \u2014 when off, visitors don\'t see a list of their past conversations; they can still start a new chat.\n- **Auto-open** \u2014 expand the widget automatically after a delay, once per visitor session.\n- **Auto-close** \u2014 close inactive conversations and tickets after a set time.\n\n## AI\n\nWith the AI chatbot enabled, you can upload a **bot avatar** that appears next to AI replies instead of the default bot icon.\n\n## Overriding from code\n\nThe dashboard is the source of truth. For per-page tweaks, a few values can be overridden in the install snippet:\n\n```html\n<script>\n window.KERConfig = {\n widgetKey: "wk_yourkey_here",\n theme: {\n primaryColor: "#6d28d9",\n secondaryColor: "#8b5cf6",\n bubbleIcon: "https://yoursite.com/logo.png" // overrides the logo\n },\n chatSettings: {\n welcomeMessage: "Hi there \u{1F44B} How can we help you today?",\n offlineMessage: "We\'re away \u2014 leave a message and we\'ll reply soon.",\n showBranding: true\n }\n };\n</script>\n```\n\nAny value set here wins over the dashboard setting on that page.\n\nTo open the chat from your own button, hide the launcher bubble, or move the widget at runtime, see the [JavaScript API](/docs/widget/javascript-api).', headings: [{ level: 2, text: "Style" }, { level: 2, text: "Launcher and layout" }, { level: 2, text: "AI page awareness" }, { level: 2, text: "Messages" }, { level: 2, text: "Behavior" }, { level: 2, text: "AI" }, { level: 2, text: "Overriding from code" }] }, { id: "widget/domain-whitelist", url: "https://kerabie.com/docs/widget/domain-whitelist", title: "Domain Whitelist", description: "Restrict your widget to only load on approved domains.", section: "widget", body: "By default a widget key works on any domain. Enable domain whitelisting to prevent your widget from being embedded on other sites.\n\n## Enable whitelisting\n\n1. Go to **Settings \u2192 Widget \u2192 Security**\n2. Toggle on **Domain whitelist**\n3. Add your allowed domains (one per line):\n ```\n yoursite.com\n www.yoursite.com\n staging.yoursite.com\n localhost\n ```\n4. Save\n\n Add `localhost` and any staging domains before enabling, or your widget will stop loading in development.\n\n## How it works\n\nThe Kerabie CDN checks the `Referer` and `Origin` headers on every widget load. If the domain isn't in your whitelist, the widget script returns an empty response.\n\nThis doesn't prevent someone from making API calls directly (use API key scoping for that), but it stops your chat bubble from appearing on unauthorized sites.", headings: [{ level: 2, text: "Enable whitelisting" }, { level: 2, text: "How it works" }] }, { id: "widget/install-html", url: "https://kerabie.com/docs/widget/install-html", title: "Install via HTML", description: "Add Kerabie to any website with two script tags.", section: "widget", body: "## Get your widget key\n\n1. Log in to [kerabie.com/app](https://kerabie.com/app)\n2. Go to **Settings \u2192 Widget**\n3. Copy your **Widget Key** (starts with `wk_`)\n\n## Add the script\n\nPaste both tags before the closing `</body>` on every page:\n\n```html\n<script>\n window.KERConfig = {\n widgetKey: \"wk_yourkey_here\"\n };\n</script>\n<script async src=\"https://cdn.kerabie.com/widget.js\"></script>\n```\n\nThat's it \u2014 the chat bubble appears immediately.\n\n## Passing user identity\n\nIf your site has logged-in users, identify them so agents know who they're talking to:\n\n```html\n<script>\n window.KERConfig = {\n widgetKey: \"wk_yourkey_here\",\n user: {\n name: \"Jane Smith\",\n email: \"jane@example.com\",\n userId: \"user_123\"\n }\n };\n</script>\n```\n\n## Load only on specific pages\n\nTo show the widget on certain pages only, wrap the second script in a condition:\n\n```html\n<script>\n window.KERConfig = { widgetKey: \"wk_yourkey_here\" };\n</script>\n<script>\n if (window.location.pathname.startsWith('/support')) {\n var s = document.createElement('script');\n s.src = 'https://cdn.kerabie.com/widget.js';\n s.async = true;\n document.body.appendChild(s);\n }\n</script>\n```\n\n## Content Security Policy\n\nIf your site uses CSP headers, add these directives:\n\n```\nscript-src 'self' https://cdn.kerabie.com;\nconnect-src 'self' https://api.kerabie.com wss://ws.kerabie.com;\nimg-src 'self' https://assets.kerabie.com data:;\nframe-src https://cdn.kerabie.com;\n```", headings: [{ level: 2, text: "Get your widget key" }, { level: 2, text: "Add the script" }, { level: 2, text: "Passing user identity" }, { level: 2, text: "Load only on specific pages" }, { level: 2, text: "Content Security Policy" }] }, { id: "widget/install-shopify", url: "https://kerabie.com/docs/widget/install-shopify", title: "Install on Shopify", description: "Add Kerabie live chat to your Shopify store \u2014 no theme code editing required.", section: "widget", body: '## Connect your store\n\nIn the Kerabie dashboard, go to **Channels \u2192 Shopify**, enter your `your-store.myshopify.com` domain, and approve the install on Shopify\'s consent screen. You\'re sent back to Kerabie with the store connected \u2014 there\'s no API key to copy.\n\n## Add the widget to your storefront\n\nEvery Online Store 2.0 theme (the default for new stores) includes a **Custom Liquid** block, so you never have to open `theme.liquid`.\n\n <Step title="Copy your install code">\n In Kerabie, go to **Settings \u2192 Widget Settings** and click **Get Install Code**, then open the **Shopify** tab and copy the snippet.\n </Step>\n <Step title="Open the theme editor">\n In Shopify admin go to **Online Store \u2192 Themes** and click **Customize** on your live theme.\n </Step>\n <Step title="Add a Custom Liquid block to the footer">\n Switch to the **Footer** group so the widget loads on every page, click **Add section** (or **Add block**), and choose **Custom Liquid**.\n </Step>\n <Step title="Paste and save">\n Paste the snippet into the Liquid box and click **Save**. The chat bubble appears across your storefront.\n </Step>\n\nThe snippet looks like this:\n\n```html\n<script>\n window.KERConfig = {\n widgetKey: "wk_yourkey_here",\n };\n</script>\n<script async src="https://cdn.kerabie.com/widget.js"></script>\n```\n\n Using an older theme without Custom Liquid? Send the snippet to whoever manages your theme code and have them paste it before the closing `</body>` tag in `Layout \u2192 theme.liquid`.\n\n## Identify logged-in customers\n\nBecause the snippet lives in a Liquid block, you can pass the signed-in customer\'s name and email so agents know who they\'re talking to:\n\n```html\n<script>\n window.KERConfig = {\n widgetKey: "wk_yourkey_here",\n {% if customer %}\n user: {\n name: "{{ customer.name | escape }}",\n email: "{{ customer.email | escape }}"\n }\n {% endif %}\n };\n</script>\n<script async src="https://cdn.kerabie.com/widget.js"></script>\n```\n\n## Show on specific pages only\n\nWrap the snippet in a Liquid condition \u2014 for example, cart and checkout only:\n\n```html\n{% if template contains \'cart\' or template contains \'checkout\' %}\n<script>\n window.KERConfig = { widgetKey: "wk_yourkey_here" };\n</script>\n<script async src="https://cdn.kerabie.com/widget.js"></script>\n{% endif %}\n```', headings: [{ level: 2, text: "Connect your store" }, { level: 2, text: "Add the widget to your storefront" }, { level: 2, text: "Identify logged-in customers" }, { level: 2, text: "Show on specific pages only" }] }, { id: "widget/install-webflow", url: "https://kerabie.com/docs/widget/install-webflow", title: "Install on Webflow / Squarespace / Wix", description: "Add Kerabie to no-code website builders.", section: "widget", body: '<Tab title="Webflow">\n 1. Open your Webflow project and go to **Project Settings \u2192 Custom Code**\n 2. In the **Footer Code** section, paste:\n ```html\n <script>\n window.KERConfig = { widgetKey: "wk_yourkey_here" };\n </script>\n <script async src="https://cdn.kerabie.com/widget.js"></script>\n ```\n 3. Click **Save** and publish your site\n </Tab>\n <Tab title="Squarespace">\n 1. Go to **Settings \u2192 Advanced \u2192 Code Injection**\n 2. In the **Footer** section, paste the two script tags\n 3. Click **Save** and preview your site\n </Tab>\n <Tab title="Wix">\n 1. Open the Wix Editor and click **Add \u2192 Embed \u2192 Custom Embed**\n 2. Click **Enter Code** and paste both script tags\n 3. Position the embed element anywhere on the page (it renders invisibly)\n 4. Publish your site\n\n <Note>Wix requires the Wix Premium plan to add custom code.</Note>\n </Tab>\n <Tab title="Framer">\n 1. Open your Framer project\n 2. Go to **Site Settings \u2192 General \u2192 Custom Code**\n 3. Paste both script tags in the **End of `<body>` tag** section\n 4. Publish\n </Tab>', headings: [] }, { id: "widget/install-wordpress", url: "https://kerabie.com/docs/widget/install-wordpress", title: "Install on WordPress", description: "Add Kerabie to WordPress in under 2 minutes \u2014 no code required.", section: "widget", body: `## Install the plugin
|
|
614
|
-
|
|
615
|
-
<Step title="Download or search">
|
|
616
|
-
In WP Admin go to **Plugins \u2192 Add New** and search **"Kerabie Live Chat"**, or download the ZIP from [kerabie.com/integrations/wordpress](https://kerabie.com/integrations/wordpress).
|
|
617
|
-
</Step>
|
|
618
|
-
<Step title="Activate">
|
|
619
|
-
Click **Install Now** then **Activate**.
|
|
620
|
-
</Step>
|
|
621
|
-
<Step title="Enter your Widget Key">
|
|
622
|
-
Go to **WP Admin \u2192 Kerabie \u2192 Settings**. Paste your Widget Key (from Kerabie dashboard \u2192 Settings \u2192 Widget).
|
|
623
|
-
</Step>
|
|
624
|
-
<Step title="Save">
|
|
625
|
-
Click **Save Settings**. The chat bubble appears on your site immediately.
|
|
626
|
-
</Step>
|
|
627
|
-
|
|
628
|
-
## Features
|
|
629
|
-
|
|
630
|
-
- **Automatic user identity** \u2014 when a WordPress user is logged in, their name and email are passed to the widget automatically. Agents see who they're talking to without asking.
|
|
631
|
-
- **Page targeting** \u2014 choose which pages show the widget (all pages, specific pages, or post types).
|
|
632
|
-
- **Color and position** \u2014 configure widget appearance directly in WP Admin.
|
|
633
|
-
- **Branding toggle** \u2014 hide "Powered by Kerabie" on Pro plan.
|
|
634
|
-
|
|
635
|
-
## Passing custom data
|
|
636
|
-
|
|
637
|
-
For WooCommerce or membership sites, you can extend the plugin to pass extra data:
|
|
638
|
-
|
|
639
|
-
\`\`\`php
|
|
640
|
-
// functions.php
|
|
641
|
-
add_filter('kerabie_user_data', function($data) {
|
|
642
|
-
if (is_user_logged_in()) {
|
|
643
|
-
$user = wp_get_current_user();
|
|
644
|
-
$data['plan'] = get_user_meta($user->ID, 'membership_level', true);
|
|
645
|
-
$data['orders'] = wc_get_customer_order_count($user->ID);
|
|
646
|
-
}
|
|
647
|
-
return $data;
|
|
648
|
-
});
|
|
649
|
-
\`\`\`
|
|
650
|
-
|
|
651
|
-
## Troubleshooting
|
|
652
|
-
|
|
653
|
-
<Accordion title="Widget not appearing">
|
|
654
|
-
Check that the Widget Key is saved correctly. Make sure no caching plugin is serving a stale version of your pages \u2014 clear your cache after installing.
|
|
655
|
-
</Accordion>
|
|
656
|
-
<Accordion title="Widget appears on pages I don't want">
|
|
657
|
-
Use the **Page Targeting** setting in WP Admin \u2192 Kerabie \u2192 Settings to restrict which pages show the widget.
|
|
658
|
-
</Accordion>
|
|
659
|
-
<Accordion title="Conflict with another chat plugin">
|
|
660
|
-
Deactivate any other live chat plugins. Two chat widgets on the same page will conflict.
|
|
661
|
-
</Accordion>`, headings: [{ level: 2, text: "Install the plugin" }, { level: 2, text: "Features" }, { level: 2, text: "Passing custom data" }, { level: 2, text: "Troubleshooting" }] }, { id: "widget/javascript-api", url: "https://kerabie.com/docs/widget/javascript-api", title: "JavaScript API", description: "Open and close the chat, hide the launcher bubble, and move the widget from your own code.", section: "widget", body: 'Once the widget has loaded, it exposes `window.Kerabie`, so you can control it from your own buttons, links and scripts.\n\n## Wait for the widget to load\n\nThe widget script loads `async`, so `window.Kerabie` doesn\'t exist yet when your own page scripts first run. Use either of these to run code once it\'s ready.\n\n**`onReady` in your install snippet:**\n\n```html\n<script>\n window.KERConfig = {\n widgetKey: "wk_yourkey_here",\n onReady: function (kerabie) {\n // kerabie === window.Kerabie\n kerabie.setPosition("bottom-left");\n }\n };\n</script>\n<script async src="https://cdn.kerabie.com/widget.js"></script>\n```\n\n**The `kerabie:ready` event**, from anywhere on the page:\n\n```js\nwindow.addEventListener("kerabie:ready", function () {\n window.Kerabie.open();\n});\n```\n\nFor code that runs on a user action (like a click), it\'s usually loaded already. Guard the call in case it isn\'t:\n\n```js\nwindow.Kerabie?.open();\n```\n\n## Methods\n\n| Method | What it does |\n|---|---|\n| `Kerabie.open()` | Opens the chat window. |\n| `Kerabie.close()` | Closes the chat window. |\n| `Kerabie.toggle()` | Opens it if closed, closes it if open. |\n| `Kerabie.isOpen()` | Returns `true` while the chat window is open. |\n| `Kerabie.hideLauncher()` | Hides the launcher bubble. The chat can still be opened with `open()`. |\n| `Kerabie.showLauncher()` | Shows the launcher bubble again. |\n| `Kerabie.setPosition(position)` | Moves the widget: `"bottom-right"`, `"bottom-left"`, `"top-right"` or `"top-left"`. |\n| `Kerabie.setConsent(granted)` | Tells the widget whether the visitor has consented to non-essential local storage. Only does anything if **Cookie Consent** is on under **Settings \u2192 Org Settings \u2192 Security and Privacy** \u2014 otherwise it\'s a no-op. |\n\n`setPosition` and `hideLauncher` apply to the current page only. Your dashboard setting under **Settings \u2192 Widget Settings \u2192 Style** stays the default everywhere else.\n\n## Cookie Consent mode\n\nThe widget doesn\'t use cookies \u2014 visitor/session data (a session id, conversation id, name/email if given, and appearance) is kept in `localStorage`. When your org enables **Cookie Consent** in Settings, the widget still works normally, but stops **writing** any of that to `localStorage` until you call `setConsent(true)` \u2014 so nothing persists across page loads for a visitor who hasn\'t consented. Call it from your own cookie bar\'s callbacks:\n\n```js\n// wherever your cookie/consent bar calls back on accept/decline/withdraw\nwindow.Kerabie?.setConsent(true) // start persisting the visitor\'s session locally\nwindow.Kerabie?.setConsent(false) // stop, and clear whatever was already stored\n```\n\n Reads are never gated \u2014 if a visitor already consented on a previous visit, they\'re still recognized on this one. Only writes wait for `setConsent(true)`.\n\n## Install snippet options\n\nThese set the same things before the widget first appears, so there\'s no flicker:\n\n| Option | Type | What it does |\n|---|---|---|\n| `position` | string | Same values as `setPosition`. Overrides the dashboard position on this page. |\n| `hideFloatingButton` | boolean | Start with the launcher bubble hidden. |\n| `defaultOpen` | boolean | Start with the chat window open on every page load. |\n| `onReady` | function | Called with `window.Kerabie` once it\'s ready. |\n\n## Examples\n\n### Open the chat from your own button\n\nHide the bubble and use a button that fits your design instead:\n\n```html\n<button id="support-btn" type="button">Chat with support</button>\n\n<script>\n window.KERConfig = {\n widgetKey: "wk_yourkey_here",\n hideFloatingButton: true\n };\n document.getElementById("support-btn").addEventListener("click", function () {\n window.Kerabie?.open();\n });\n</script>\n<script async src="https://cdn.kerabie.com/widget.js"></script>\n```\n\n### Open the chat from a link\n\n```html\n<a href="#" onclick="window.Kerabie?.open(); return false;">Talk to us</a>\n```\n\n### Move the widget on one page\n\nFor example, out of the way of a "Buy" bar on product pages:\n\n```js\nwindow.addEventListener("kerabie:ready", function () {\n if (location.pathname.startsWith("/products/")) {\n window.Kerabie.setPosition("bottom-left");\n }\n});\n```\n\n Building a React Native app? The React Native SDK has the same controls: `open`, `close` and `toggle` from `useKerAbieContext()`, plus the `position` and `hideFloatingButton` props on `KerAbieProvider`. See [React Native SDK](/docs/sdks/react-native).', headings: [{ level: 2, text: "Wait for the widget to load" }, { level: 2, text: "Methods" }, { level: 2, text: "Cookie Consent mode" }, { level: 2, text: "Install snippet options" }, { level: 2, text: "Examples" }, { level: 3, text: "Open the chat from your own button" }, { level: 3, text: "Open the chat from a link" }, { level: 3, text: "Move the widget on one page" }] }, { id: "widget/offline-mode", url: "https://kerabie.com/docs/widget/offline-mode", title: "Offline Mode", description: "What visitors see when your team isn't available.", section: "widget", body: "The widget shows your team as **offline** when either:\n\n- no agent is signed in to Kerabie (on the web dashboard or mobile app), or\n- you've set the availability switcher at the top of **Settings \u2192 Widget Settings** to **Away**.\n\n**Away** is a manual override \u2014 use it to go offline for holidays or after hours even while agents are still signed in. It takes effect for visitors right away, no save needed. The switcher shows **No agents online** when it's set to Available but nobody is signed in.\n\n## What visitors see\n\nWhile you're offline, the chat window shows your **offline message** (set under **Settings \u2192 Widget Settings \u2192 Messages**). Visitors can still send messages; they land in your inbox for when you're back.\n\n If the AI chatbot is enabled, the offline message isn't shown \u2014 the bot keeps answering whether or not an agent is available.\n\n## Email follow-up\n\nIf a visitor who has shared their email messages you while you're offline, Kerabie emails them to confirm their message was received, so they know to watch their inbox for your reply.", headings: [{ level: 2, text: "What visitors see" }, { level: 2, text: "Email follow-up" }] }, { id: "widget/overview", url: "https://kerabie.com/docs/widget/overview", title: "Chat Widget", description: "The Kerabie chat widget runs on any website and connects visitors to your team in real time.", section: "widget", body: 'The widget is a lightweight JavaScript snippet (~8 KB gzipped). It renders a chat bubble on your site, opens a conversation panel, and connects via WebSocket for real-time messaging.\n\n## Installation methods\n\n| Method | Best for |\n|---|---|\n| [HTML script tag](/docs/widget/install-html) | Any static site, custom builds |\n| [WordPress plugin](/docs/widget/install-wordpress) | WordPress sites |\n| [Webflow / Squarespace / Wix](/docs/widget/install-webflow) | No-code builders |\n| [Shopify](/docs/widget/install-shopify) | E-commerce |\n| [React Native SDK](/docs/sdks/react-native) | Mobile apps |\n\n## Identifying visitors\n\nPass user data so agents see who they\'re talking to:\n\n```html\n<script>\n window.KERConfig = {\n widgetKey: "wk_yourkey_here",\n user: {\n name: "Jane Smith",\n email: "jane@example.com",\n userId: "user_123", // your internal ID\n plan: "pro", // any custom attribute\n }\n };\n</script>\n<script async src="https://cdn.kerabie.com/widget.js"></script>\n```\n\n## JavaScript API\n\nControl the widget programmatically:\n\n```javascript\n// Open / close\nwindow.Kerabie.open();\nwindow.Kerabie.close();\nwindow.Kerabie.toggle();\n\n// Unread count\nwindow.Kerabie.on(\'unreadCount\', (count) => {\n document.title = count > 0 ? `(${count}) My App` : \'My App\';\n});\n\n// Pre-fill a message\nwindow.Kerabie.openWithMessage(\'I need help with my order\');\n```\n\n## Available plans\n\n| Feature | Free | Starter | Pro | Business |\n|---|---|---|---|---|\n| Widget on unlimited pages | \u2713 | \u2713 | \u2713 | \u2713 |\n| Remove "Powered by Kerabie" | \u2014 | \u2014 | \u2713 | \u2713 |\n| Custom pages in widget | \u2014 | \u2014 | \u2713 | \u2713 |\n| Domain whitelist | \u2713 | \u2713 | \u2713 | \u2713 |', headings: [{ level: 2, text: "Installation methods" }, { level: 2, text: "Identifying visitors" }, { level: 2, text: "JavaScript API" }, { level: 2, text: "Available plans" }] }, { id: "readme/typescript-sdk", url: "", title: "TypeScript SDK README", description: "The TypeScript SDK package readme, straight from its repository.", section: "sdks", body: "# @kerabie/sdk\n\nOfficial TypeScript SDK for the [Kerabie](https://kerabie.com) customer support platform.\n\n## Installation\n\n```bash\nnpm install @kerabie/sdk\n```\n\n## Quick Start\n\n```typescript\n\nconst kerabie = new KerAbieClient({\n apiKey: process.env.KERABIE_API_KEY,\n orgId: 'your-org-id', // Optional; set per request or in client\n baseUrl: 'https://api.kerabie.com', // Default\n});\n```\n\n## Conversations\n\n```typescript\n// List conversations\nconst { data, meta } = await kerabie.conversations.list({ status: 'OPEN', limit: 20 });\n\n// Get a single conversation\nconst conv = await kerabie.conversations.get(123);\n\n// Send a message\nconst msg = await kerabie.conversations.sendMessage(123, { body: 'Hello!' });\n\n// Resolve / assign\nawait kerabie.conversations.resolve(123);\nawait kerabie.conversations.assign(123, agentId);\n```\n\n## Customers\n\n```typescript\n// Upsert a customer (create or update)\nconst customer = await kerabie.customers.upsert({\n email: 'user@example.com',\n name: 'John Doe',\n externalRef: 'app_user_id_123',\n customAttributes: { plan: 'pro' },\n});\n\n// Get conversation history\nconst { data: convs } = await kerabie.customers.getConversations(customer.id);\n```\n\n## Campaigns\n\n```typescript\n// Proactive messages on your chat widget\nconst { campaigns, meta } = await kerabie.campaigns.list({ status: 'active' });\n\n// Saved as a draft; add isActive: true to launch it straight away\nconst { campaign } = await kerabie.campaigns.create({\n name: 'Pricing page nudge',\n message: 'Questions about plans? I can compare them for you in two minutes.',\n ctaLabel: 'Compare plans',\n trigger: 'time_on_page',\n triggerValue: 20, // seconds\n pages: '/pricing',\n audience: 'all',\n frequency: 'once_per_visitor',\n});\n\nawait kerabie.campaigns.update(campaign.id, { isActive: true }); // launch (403 if your plan's live limit is reached)\nawait kerabie.campaigns.duplicate(campaign.id); // copy as a draft\nconst stats = await kerabie.campaigns.getStats(); // { total, active, sent, opened, clicked, replied, limit, \u2026 }\nawait kerabie.campaigns.delete(campaign.id);\n```\n\n`trigger` is `page_load`, `time_on_page` (seconds), `scroll_depth` (percent) or `exit_intent`. `pages` takes comma-separated URL patterns with `*` wildcards; leave it empty for every page. How many campaigns can be live at once depends on your plan (`getStats().limit`, 0 = unlimited).\n\n## AI tools\n\n```typescript\n// Custom API actions your chatbot can call (check stock, validate an address, book a slot\u2026)\nconst { tools, stats } = await kerabie.aiTools.list(); // stats.limit: -1 = not on your plan, 0 = unlimited\n\nconst { tool } = await kerabie.aiTools.create({\n name: 'check_availability', // snake_case; this is what the AI calls\n displayName: 'Check availability',\n description: 'Use this when a customer asks if a specific item is in stock.',\n endpoint: 'https://api.yourstore.com/inventory/check', // must be a public address\n method: 'GET',\n authType: 'bearer',\n authConfig: { token: process.env.STORE_API_TOKEN }, // write-only, never returned\n responsePath: 'data.message',\n parameters: [\n { name: 'sku', type: 'string', description: 'Product SKU the customer mentioned' },\n { name: 'location', type: 'string', description: 'Store, if mentioned', required: false },\n ],\n});\n\nconst result = await kerabie.aiTools.test(tool.id, { sku: 'A-100' }); // calls your real endpoint once\nawait kerabie.aiTools.update(tool.id, { enabled: false }); // switch it off\nawait kerabie.aiTools.duplicate(tool.id); // a copy that starts switched off\nawait kerabie.aiTools.delete(tool.id);\n```\n\nHow many tools you can have depends on your plan; `create` and `duplicate` throw a 403 when it is full. Endpoints that resolve to a private or local address are rejected, and redirects are not followed.\n\n### MCP servers as tool sources\n\n```typescript\n// Let your chatbot use the tools of a remote MCP server\nconst { server } = await kerabie.mcpServers.create({\n name: 'Shop tools',\n description: 'Order and stock lookups for our shop. Use when a customer asks about an order or an item.',\n url: 'https://mcp.yourstore.com/mcp',\n authType: 'bearer',\n authConfig: { token: process.env.SHOP_MCP_TOKEN },\n});\nconsole.log(server.tools.map((t) => t.name)); // what the server offers\n\nawait kerabie.mcpServers.update(server.id, { enabledTools: ['check_stock'] }); // switch tools on\nawait kerabie.mcpServers.test(server.id, 'check_stock', { sku: 'A-100' });\nawait kerabie.mcpServers.sync(server.id); // read the tools again\nawait kerabie.mcpServers.delete(server.id);\n```\n\nEach switched-on MCP tool counts towards your plan's AI tool limit, the same as your own tools.\n\n## Contacts\n\n```typescript\n// People you have talked to (or added), last-contacted first\nconst { contacts, meta } = await kerabie.contacts.list({ type: 'VIP', search: 'palmex', limit: 20 });\n\nconst stats = await kerabie.contacts.getStats(); // { total, newThisWeek, leads, vip }\n\n// Add one by hand \u2014 throws an HttpError with status 409 if the email is already saved\nconst { contact } = await kerabie.contacts.create({ name: 'Chidi Okafor', email: 'chidi@palmex.com', company: 'Palmex', contactType: 'CUSTOMER' });\n\nawait kerabie.contacts.update(contact.id, { contactType: 'VIP', notes: 'Prefers WhatsApp', ownerId: 7 });\n\n// Bulk import (max 1,000 per call). Existing emails are skipped, bad rows are reported by number.\nconst result = await kerabie.contacts.import([{ name: 'Sam Adeyemi', email: 'sam@tenda.ng', type: 'lead' }]);\nresult; // { created, duplicates, invalid: [{ row, error }], total }\n```\n\n`type` is `LEAD`, `CUSTOMER` or `VIP`. `ownerId` is a team-member id and must belong to your organisation. Emails are stored lower-cased and must be unique per organisation.\n\n## Visitors\n\n```typescript\n// Who has been on the site recently (online and offline), newest first\nconst { data, meta } = await kerabie.visitors.list({ status: 'all', country: 'NG', limit: 20 });\ndata[0].visitCount; // 14 \u2192 coming back after 30+ minutes away counts as a new visit\ndata[0].status; // 'online' | 'offline' | 'blocked' | 'allowed'\n\n// On the site right now\nconst { data: online } = await kerabie.visitors.getActive();\n\n// Online now, countries and returning visitors (24h), blocked IPs, top countries\nconst overview = await kerabie.visitors.getOverview();\n\n// CSV of the visitors matching the filters (max 5,000 rows) \u2014 Pro plan and above\nconst { csv, filename, count, truncated } = await kerabie.visitors.export({ status: 'blocked' });\n```\n\n`status` comes from your IP rules and wins over presence: an allowlisted IP reads `allowed` even while the visitor is online. `ip` is `null` when your organisation hides visitor IPs in its privacy settings. Visitor history is kept for 30 days. `visitors.export()` needs a plan with data exports; on other plans it throws an `HttpError` with status `403`.\n\n## Knowledge Base\n\n```typescript\nconst { data: articles } = await kerabie.kb.search('refund policy');\n\nconst stats = await kerabie.kb.getStats(); // { total, published, drafts, views, top, categories }\nconst insights = await kerabie.kb.getInsights(); // { mostViewed, gaps, lowestRated, stale }: what to write or fix next\n\nconst article = await kerabie.kb.createArticle({\n title: 'How to reset your password',\n body: '...markdown content...',\n category: 'account',\n tags: ['password', 'security'],\n});\n```\n\n## Tickets\n\nTickets are async, form-submitted support requests \u2014 the same underlying conversation model as live chat, just created through a dedicated flow instead of a real-time widget session. Use `externalRef` to identify your own end users; repeated calls with the same `externalRef` land on the same open ticket instead of creating duplicates.\n\n```typescript\n// Create a ticket on behalf of one of your users\nconst { ticketId } = await kerabie.tickets.create({\n externalRef: 'user_42', // your own stable user id\n subject: 'Refund request',\n message: 'I was charged twice for order #1029.',\n priority: 'HIGH', // LOW | NORMAL | HIGH | URGENT\n name: 'Jane Doe',\n email: 'jane@example.com', // optional \u2014 enables email continuation: replies to the notification land back on this ticket\n});\n\n// List / filter\nconst { data: tickets } = await kerabie.tickets.list({ status: 'WAITING', priority: 'HIGH' });\n\n// Fetch a ticket with its full message thread\nconst ticket = await kerabie.tickets.get(ticketId);\n\n// Reply as the end user (e.g. relaying a message from your own app)\nawait kerabie.tickets.reply(ticketId, 'Any update on this?');\n\n// Update status / priority, or use the shortcuts\nawait kerabie.tickets.update(ticketId, { priority: 'URGENT' });\nawait kerabie.tickets.resolve(ticketId);\nawait kerabie.tickets.close(ticketId);\n\n// Overview numbers: open, unassigned, breaching SLA, and 7-day average resolution time\nconst stats = await kerabie.tickets.getStats();\nstats.resolutionDeltaPct; // -18 \u2192 18% faster than the 7 days before (null if nothing to compare)\n\n// CSV export of the tickets matching the filters (max 5,000 rows)\nconst { csv, filename, count, truncated } = await kerabie.tickets.export({ status: 'ACTIVE', priority: 'URGENT' });\n```\n\n`tickets.export()` needs a plan with data exports (Pro and above); on other plans it throws an `HttpError` with status `403`. Every cell is escaped, and values that start with `=`, `+`, `-` or `@` are prefixed with a single quote so spreadsheets show them as text instead of running them as formulas.\n\nTickets are subject to the same monthly conversation limit as live chat on your plan (a ticket **is** a conversation) \u2014 there's no separate ticket-specific cap or plan gate on the API itself.\n\n## Webhooks\n\n```typescript\n// Register a webhook\nconst webhook = await kerabie.webhooks.create({\n url: 'https://your-server.com/hooks/kerabie',\n events: ['message.sent.customer', 'conversation.created'],\n secret: 'your-signing-secret',\n});\n\n// Verify incoming webhooks (Express example)\n\nconst app = express();\napp.use(express.raw({ type: 'application/json' }));\n\napp.post('/hooks/kerabie', (req, res) => {\n const sig = req.headers['x-kerabie-signature'] as string;\n const rawBody = req.body.toString();\n\n if (!WebhooksResource.verify(rawBody, sig, process.env.KERABIE_WEBHOOK_SECRET!)) {\n return res.status(401).end();\n }\n\n const payload = WebhooksResource.parse(rawBody);\n console.log('Event:', payload.event, payload.data);\n res.status(200).end();\n});\n```\n\n## Analytics\n\nThe same numbers as your Kerabie dashboard (cached for up to 5 minutes; tickets and setup for 1 minute).\n\n```typescript\n// Conversation, response-time, satisfaction and AI numbers (days: default 30, max 365)\nconst summary = await kerabie.analytics.getSummary({ days: 7 });\nsummary.totals.conversations; // 186\nsummary.deltas.conversations; // 12 \u2192 +12% vs the previous 7 days (null if nothing to compare)\nsummary.ai.resolvedAlonePct; // 70 \u2192 % the AI handled without a human\nsummary.ai.agentHoursSaved; // 41 \u2192 an estimate\nsummary.trend; // [{ date, count, ai, human }, \u2026] oldest first\n\n// Team\nconst { agents } = await kerabie.analytics.getAgentStats({ period: '7d' });\n\n// The deeper numbers behind the dashboard's Analytics page\nconst insights = await kerabie.analytics.getInsights({ days: 30 });\ninsights.firstResponse.medianSeconds; // 144\ninsights.ai.questions[0]; // { question, asked, aiResolvedPct, avgRating, page, trendPct }\ninsights.customers.heatmap.cells; // 7 weekdays \xD7 12 two-hour blocks of customer messages\ninsights.pages[0]; // { page, views, questions, askRate, topQuestion }\n\n// Open tickets by priority and the % still within SLA\nconst sla = await kerabie.analytics.getTicketSla();\nsla.withinSlaPct; // 94\nsla.byPriority; // [{ priority: 'URGENT', count: 3, oldestMinutes: 14 }, \u2026]\n\n// First-run checklist: connect a channel, train the AI, create an automation\nconst setup = await kerabie.analytics.getSetupStatus(); // { channel, ai, automation }\n```\n\n`deltas.responseTime` is a percentage where a **negative** number is an improvement. `avgResponseTime` is in seconds.\n\n## AI take-over and customer history\n\n```typescript\n// List with inbox tab counts, tags and an `aiHandling` flag\nconst { data, meta } = await kerabie.conversations.list({ status: 'WAITING' });\nmeta.counts; // { all, WAITING, ACTIVE, RESOLVED, CLOSED } \u2014 ignores filters\ndata.filter((c) => c.aiHandling); // conversations the AI is handling alone\n\n// A customer's earlier conversations (newest first, default 5, max 20)\nconst { data: earlier } = await kerabie.conversations.getHistory(101, 5);\n\n// Stop the AI and hand the conversation to a team member\nawait kerabie.conversations.takeOver(101, { agentId: 7 });\n```\n\n`takeOver` throws an `HttpError` with status `400` if the AI is no longer handling the conversation (for example someone already took over), so it is safe to call from several places at once.\n\n## Error handling\n\n```typescript\n\ntry {\n const conv = await kerabie.conversations.get(9999);\n} catch (err) {\n if (err instanceof HttpError) {\n console.log(err.status); // 404\n console.log(err.message); // \"Conversation not found\"\n }\n}\n```\n\n## TypeScript\n\nAll types are exported:\n\n```typescript\n\n```\n\n## License\n\nMIT\n\n## Building with an AI assistant\n\nThe [`@kerabie/mcp`](https://kerabie.com/docs/sdks/mcp) server gives Claude, Cursor and other MCP clients the API reference, this SDK's types and integration guides, so generated code matches the real API. It needs no API key to help you build, and with one it can work with your account:\n\n```json\n{ \"mcpServers\": { \"kerabie\": { \"command\": \"npx\", \"args\": [\"-y\", \"@kerabie/mcp\"], \"env\": { \"KERABIE_API_KEY\": \"ker_live_...\" } } } }\n```", headings: [{ level: 2, text: "Installation" }, { level: 2, text: "Quick Start" }, { level: 2, text: "Conversations" }, { level: 2, text: "Customers" }, { level: 2, text: "Campaigns" }, { level: 2, text: "AI tools" }, { level: 3, text: "MCP servers as tool sources" }, { level: 2, text: "Contacts" }, { level: 2, text: "Visitors" }, { level: 2, text: "Knowledge Base" }, { level: 2, text: "Tickets" }, { level: 2, text: "Webhooks" }, { level: 2, text: "Analytics" }, { level: 2, text: "AI take-over and customer history" }, { level: 2, text: "Error handling" }, { level: 2, text: "TypeScript" }, { level: 2, text: "License" }, { level: 2, text: "Building with an AI assistant" }] }, { id: "readme/react-native-sdk", url: "", title: "React Native SDK README", description: "The React Native SDK package readme, straight from its repository.", section: "sdks", body: '# react-native-kerabie-helpdesk\n\nEmbed the [Kerabie](https://kerabie.com) help desk into any React Native or Expo app.\nFull TypeScript support, real-time chat, push notifications, contextual help, and multi-app analytics.\n\n## Contents\n\n- [Installation](#installation)\n- [Provider setup](#provider-setup)\n- [KerAbieProvider props](#kerAbieProvider-props)\n- [Hooks](#hooks)\n - [useKerAbieChat](#usekerAbieChat)\n - [useKerAbieUser](#usekerAbieUser)\n - [useKerAbieApp](#usekerAbieApp)\n - [useKerAbieNotifications](#usekerAbieNotifications)\n- [Components](#components)\n - [KerAbieFloatingButton](#kerAbieFloatingButton)\n - [KerAbieChatScreen](#kerAbieChatScreen)\n- [Types](#types)\n- [Open modes in depth](#open-modes-in-depth)\n- [Campaigns](#campaigns)\n- [Notification setup](#notification-setup)\n - [Android \u2014 register channels first](#android--register-channels-first)\n - [One-call setup](#one-call-setup)\n- [Contextual chat](#contextual-chat)\n - [Product click](#product-click)\n - [Order inquiry](#order-inquiry)\n - [Fintech transaction](#fintech-transaction)\n - [Ride sharing](#ride-sharing)\n- [User identity](#user-identity)\n\n---\n\n## Installation\n\n```bash\n# Expo managed workflow\nnpx expo install react-native-kerabie-helpdesk @react-native-async-storage/async-storage\n\n# Bare React Native\nnpm install react-native-kerabie-helpdesk @react-native-async-storage/async-storage\nnpx pod-install\n```\n\nOptional peer dependencies (only install what you use):\n\n```bash\n# Required for openMode="sheet"\nnpx expo install @gorhom/bottom-sheet react-native-reanimated react-native-gesture-handler\n\n# Required for push notifications\nnpx expo install expo-notifications\n```\n\n---\n\n## Provider setup\n\n`KerAbieProvider` must be an **ancestor** of every component that calls a `useKerAbie*` hook.\nPlace it below your gesture/safe-area wrappers but above your navigation stack.\n\n### Expo Router (`app/_layout.tsx`)\n\n```tsx\n\nexport default function RootLayout() {\n return (\n <GestureHandlerRootView style={{ flex: 1 }}>\n <SafeAreaProvider>\n <KerAbieProvider {/* \u2190 below gesture/safe-area */}\n widgetKey="wk_yourorg_123"\n appName="My App"\n appVersion={Constants.expoConfig?.version ?? \'1.0.0\'}\n openMode="modal"\n >\n <Stack /> {/* \u2190 above navigation */}\n </KerAbieProvider>\n </SafeAreaProvider>\n </GestureHandlerRootView>\n )\n}\n```\n\n### React Navigation (`App.tsx`)\n\nFor `openMode="push"`, `KerAbieProvider` must be **inside** `NavigationContainer`.\nFor `openMode="modal"` it can go either side, but inside is safer.\n\n```tsx\n\nexport default function App() {\n return (\n <GestureHandlerRootView style={{ flex: 1 }}>\n <NavigationContainer>\n <KerAbieProvider widgetKey="wk_yourorg_123" appName="My App" appVersion="1.0.0">\n <MyRootNavigator />\n </KerAbieProvider>\n </NavigationContainer>\n </GestureHandlerRootView>\n )\n}\n```\n\n---\n\n## KerAbieProvider props\n\n### Required\n\n| Prop | Type | Description |\n| ---- | ---- | ----------- |\n| `widgetKey` | `string` | Your widget key from Kerabie dashboard \u2192 Settings \u2192 Chat Widget. Format: `wk_yourorg_xxx`. |\n| `children` | `ReactNode` | Your app tree. |\n\n### App identity\n\n| Prop | Type | Default | Description |\n| ---- | ---- | ------- | ----------- |\n| `appName` | `string` | device package name | Human-readable app name shown in the agent sidebar and analytics filters (e.g. `"Delivery App"`). |\n| `appVersion` | `string` | `"0.0.0"` | Semver string. Use `Constants.expoConfig?.version` to set automatically. |\n| `appBuild` | `string` | `""` | iOS build number or Android `versionCode`. |\n\n### Open mode\n\n| Prop | Type | Default | Description |\n| ---- | ---- | ------- | ----------- |\n| `openMode` | `"modal" \\| "sheet" \\| "push"` | `"modal"` | How the chat screen opens. See [Open modes in depth](#open-modes-in-depth). |\n| `navigation` | `"expo-router" \\| "react-navigation" \\| "custom"` | \u2014 | Required when `openMode="push"`. |\n| `navigationRef` | `any` | \u2014 | Your navigation ref (React Navigation only). |\n| `onOpenChat` | `() => void` | \u2014 | Called when chat opens. Use with `openMode="custom"` to navigate yourself. |\n| `onCloseChat` | `() => void` | \u2014 | Called when chat closes. |\n\n### Floating button\n\n| Prop | Type | Default | Description |\n| ---- | ---- | ------- | ----------- |\n| `position` | `"bottom-right" \\| "bottom-left" \\| "top-right" \\| "top-left"` | `"bottom-right"` | Position of the auto-rendered floating button. |\n| `defaultOpen` | `boolean` | `false` | Open the chat immediately on mount. |\n| `hideFloatingButton` | `boolean` | `false` | Hide the auto-rendered button. Use your own trigger with `useKerAbieChat().open()`. |\n\n### User identity\n\n| Prop | Type | Description |\n| ---- | ---- | ----------- |\n| `user` | `KerUser` | Initial visitor identity. Can also be set at runtime with `useKerAbieUser().setUser()`. |\n\n### Appearance\n\nAll fields inside the `theme` object are optional.\n\n| Field | Type | Default | Description |\n| ----- | ---- | ------- | ----------- |\n| `theme.primaryColor` | `string` | `"rgb(78 121 105)"` | Accent colour for buttons, links, and agent bubbles. |\n| `theme.backgroundColor` | `string` | `"#ffffff"` | Chat screen background. |\n| `theme.textColor` | `string` | `"#0f172a"` | Body text colour. |\n| `theme.bubbleColor` | `string` | same as `primaryColor` | Floating button background colour. |\n| `theme.bubbleIcon` | `ImageSourcePropType` | \u{1F4AC} emoji | Custom floating button icon. |\n| `theme.bubbleSize` | `number` | `56` | Floating button diameter in points. |\n| `theme.fontFamily` | `string` | system default | Font family applied to all widget text. |\n| `theme.agentBubbleColor` | `string` | `"#f1f5f9"` | Background colour of agent message bubbles. |\n| `theme.userBubbleColor` | `string` | same as `primaryColor` | Background colour of visitor message bubbles. |\n\n### Chat settings (override dashboard)\n\n| Field | Type | Description |\n| ----- | ---- | ----------- |\n| `chatSettings.welcomeMessage` | `string` | Text shown before the first message. `{{name}}` is replaced by the identified user\'s first name (dropped when unknown). |\n| `chatSettings.offlineMessage` | `string` | Placeholder shown in the input when disconnected. |\n| `chatSettings.showBranding` | `boolean` | Show "Powered by Kerabie" footer (Pro feature: set to `false` to hide). |\n| `chatSettings.enableFileAttachments` | `boolean` | Allow visitors to send file attachments. |\n| `chatSettings.enableVoiceMessages` | `boolean` | Allow visitors to record voice messages. |\n\n### Notifications\n\n| Field | Type | Description |\n| ----- | ---- | ----------- |\n| `notifications.provider` | `"expo"` | Push provider. Only `"expo"` is supported currently. |\n| `notifications.getToken` | `() => Promise<string>` | Override the default Expo token fetch. |\n| `notifications.onTokenRegister` | `(token: string) => void` | Called after the token is registered with Kerabie. |\n| `notifications.onNotificationReceived` | `(n: KerNotification) => void` | Called when a Kerabie push notification arrives. |\n\n---\n\n## Hooks\n\nAll hooks must be called inside a component that is a descendant of `KerAbieProvider`.\n\n### useKerAbieChat\n\nPrimary hook for controlling the chat and reading its state.\n\n```tsx\nconst {\n isOpen, // boolean \u2014 is the chat screen currently visible?\n open, // () => void \u2014 open the chat\n close, // () => void \u2014 close the chat\n toggle, // () => void \u2014 toggle open/close\n unreadCount, // number \u2014 messages received while chat was closed\n conversations, // KerConversation[] \u2014 visitor\'s conversation list\n currentConversation, // KerConversation | null\n messages, // KerMessage[] \u2014 messages in the active conversation\n sendMessage, // (body: string, attachments?: KerAttachment[]) => Promise<void>\n isTyping, // boolean \u2014 an agent is currently typing\n onlineAgents, // number \u2014 agents currently online\n isConnected, // boolean \u2014 socket connected to Kerabie\n} = useKerAbieChat()\n```\n\n**Example \u2014 custom help button with unread badge:**\n\n```tsx\nfunction HelpButton() {\n const { open, unreadCount } = useKerAbieChat()\n return (\n <Pressable onPress={open}>\n <Text>Help</Text>\n {unreadCount > 0 && <Text>{unreadCount}</Text>}\n </Pressable>\n )\n}\n```\n\n---\n\n### useKerAbieUser\n\nManage visitor identity. Call `setUser` after login and `clearUser` after logout.\n\n```tsx\nconst {\n currentUser, // KerUser | null \u2014 currently identified visitor\n setUser, // (user: KerUser) => void\n clearUser, // () => void \u2014 resets to anonymous session\n updateAttribute, // (key: string, value: string) => void\n} = useKerAbieUser()\n```\n\n`KerUser` fields:\n\n| Field | Type | Description |\n| ----- | ---- | ----------- |\n| `id` | `string` | Your internal user ID. |\n| `name` | `string` | Visitor name shown to agents. |\n| `email` | `string` | Visitor email. |\n| `phone` | `string` | Visitor phone number. |\n| `role` | `string` | e.g. `"customer"`, `"driver"`, `"admin"`. |\n| `customAttributes` | `Record<string, string>` | Any extra key-value pairs shown in the agent sidebar. |\n\n**Example:**\n\n```tsx\nconst { setUser, clearUser, updateAttribute } = useKerAbieUser()\n\n// On login\nsetUser({ id: \'u_123\', name: \'Ada Obi\', email: \'ada@example.com\', role: \'customer\' })\n\n// Attach live context \u2014 appears in agent sidebar immediately\nupdateAttribute(\'plan\', \'pro\')\nupdateAttribute(\'last_order\', \'#ORD-4521\')\n\n// On logout\nclearUser()\n```\n\n---\n\n### useKerAbieApp\n\nRead or update app identity at runtime (e.g. after an OTA update changes the version).\n\n```tsx\nconst {\n appName, // string\n appVersion, // string\n appBuild, // string\n setAppVersion, // (v: string) => void\n} = useKerAbieApp()\n```\n\n---\n\n### useKerAbieNotifications\n\nManage Expo push notification permissions and token registration.\n\n```tsx\nconst {\n hasPermission, // boolean \u2014 user has granted push permission\n requestPermission,// () => Promise<boolean> \u2014 request OS permission\n registerToken, // (token: string) => void \u2014 send token to Kerabie backend\n} = useKerAbieNotifications()\n```\n\n**Example \u2014 full push setup alongside your existing notification config:**\n\n```tsx\n// Must be called inside a component that is inside KerAbieProvider\nfunction NotificationSetup() {\n const { requestPermission, registerToken } = useKerAbieNotifications()\n\n useEffect(() => {\n requestPermission().then(async (granted) => {\n if (!granted) return\n const { data: token } = await Notifications.getExpoPushTokenAsync()\n registerToken(token)\n })\n }, [])\n\n return null\n}\n\n// In your layout, render it as a sibling of your navigation stack:\n\n <NotificationSetup />\n <Stack />\n\n```\n\n> `NotificationSetup` must be **inside** `KerAbieProvider` so `useKerAbieNotifications()` can access the context.\n\n---\n\n## Components\n\n### KerAbieFloatingButton\n\nThe floating chat bubble. Rendered automatically by the provider unless `hideFloatingButton` is set. Use this component directly if you need to render it in a specific place (e.g. inside a tab bar).\n\n```tsx\n\n```\n\n| Prop | Type | Default | Description |\n| ---- | ---- | ------- | ----------- |\n| `size` | `number` | `56` | Button diameter in points. |\n| `badgeColor` | `string` | `"#ef4444"` | Unread count badge background colour. |\n| `style` | `ViewStyle` | \u2014 | Extra style applied to the wrapper `View`. |\n\n---\n\n### KerAbieChatScreen\n\nFull chat screen. Used automatically by the provider for `openMode="modal"`. Use it directly when `openMode="push"` and you render it in your own navigator screen.\n\n```tsx\n\n<KerAbieChatScreen\n onClose={() => navigation.goBack()}\n theme={{ primaryColor: \'#6d28d9\' }}\n/>\n```\n\n| Prop | Type | Required | Description |\n| ---- | ---- | -------- | ----------- |\n| `onClose` | `() => void` | Yes | Called when the user taps the close/back button. |\n| `theme` | `KerTheme` | No | Theme override (merged with provider theme). |\n| `chatSettings` | `KerChatSettings` | No | Override welcome/offline messages. |\n\n---\n\n## Types\n\n```tsx\n\n KerAbieProviderProps,\n KerUser,\n KerTheme,\n KerChatSettings,\n KerNotificationConfig,\n KerMessage,\n KerAttachment,\n KerConversation,\n KerNotification,\n OpenMode, // "modal" | "sheet" | "push"\n BubblePosition, // "bottom-right" | "bottom-left" | "top-right" | "top-left"\n NavigationAdapter, // "expo-router" | "react-navigation" | "custom"\n} from \'react-native-kerabie-helpdesk\'\n```\n\n#### `KerMessage`\n\n| Field | Type | Description |\n| ----- | ---- | ----------- |\n| `id` | `number \\| string` | Message ID. |\n| `body` | `string` | Message text. |\n| `createdAt` | `string` | ISO 8601 timestamp. |\n| `senderType` | `"visitor" \\| "agent" \\| "bot"` | Who sent the message. |\n| `senderName` | `string` | Display name (agents/bot only). |\n| `senderAvatar` | `string` | Avatar URL. |\n| `attachments` | `KerAttachment[]` | File/image attachments. |\n| `status` | `"sent" \\| "delivered" \\| "read" \\| "failed"` | Delivery status (visitor messages). |\n\n#### `KerConversation`\n\n| Field | Type | Description |\n| ----- | ---- | ----------- |\n| `id` | `number` | Conversation ID. |\n| `status` | `"open" \\| "resolved" \\| "closed"` | Current status. |\n| `assignedAgent` | `{ id, name, avatar? }` | Assigned agent, if any. |\n| `lastMessage` | `KerMessage` | Most recent message. |\n| `unreadCount` | `number` | Unread message count. |\n| `createdAt` | `string` | ISO timestamp. |\n| `updatedAt` | `string` | ISO timestamp. |\n\n#### `KerAttachment`\n\n| Field | Type | Description |\n| ----- | ---- | ----------- |\n| `id` | `string` | Attachment ID. |\n| `url` | `string` | Download/display URL. |\n| `type` | `"image" \\| "file" \\| "audio" \\| "video"` | Media type. |\n| `name` | `string` | File name. |\n| `size` | `number` | File size in bytes. |\n\n---\n\n## Open modes in depth\n\n### `openMode="modal"` \u2014 recommended default\n\nRenders a React Native `<Modal>` over your app. Works with **any** navigation setup, no extra configuration.\n\n```tsx\n\n <YourApp />\n\n```\n\n### `openMode="sheet"` \u2014 bottom sheet\n\nRequires `@gorhom/bottom-sheet`. Chat slides up from the bottom; the content behind stays visible.\n\n```bash\nnpx expo install @gorhom/bottom-sheet react-native-reanimated react-native-gesture-handler\n```\n\n```tsx\n\n <YourApp />\n\n```\n\n### `openMode="push"` \u2014 navigation stack\n\nPushes the chat screen onto your existing stack. The provider must be inside `NavigationContainer` (React Navigation) or at root level (Expo Router).\n\n**Expo Router:**\n```tsx\n\n <Stack />\n\n```\n\n**React Navigation:**\n```tsx\n\n <MyNavigator />\n\n```\n\n**Custom (any open mode) \u2014 control it yourself:**\n```tsx\n<KerAbieProvider\n widgetKey="wk_xxx"\n openMode="modal"\n hideFloatingButton\n onOpenChat={() => console.log(\'opened\')}\n onCloseChat={() => console.log(\'closed\')}\n>\n <YourApp />\n\n// Then from any component:\nconst { open } = useKerAbieChat()\n\n```\n\n---\n\n## Campaigns\n\nProactive messages you create in the dashboard (**Campaigns**) also appear in your app, above the launcher, with their button. The provider loads your live campaigns once per app launch; there is nothing to add to your code beyond the optional props below.\n\n| Dashboard setting | In the app |\n| ---- | ---- |\n| Trigger **Page load** | Shown about 1.5 seconds after a screen appears (each time the `screen` prop changes). |\n| Trigger **Time on page** | Shown after N seconds in the app. |\n| Trigger **Scroll depth**, **Exit intent** | Website-only. These campaigns are not shown in apps. |\n| **On pages** | Matched against the `screen` prop (for example Expo Router\'s `usePathname()`). Without the prop, only campaigns for every screen show. |\n| **Audience** | New / returning means the first / a later app launch on this device; "hasn\'t chatted" checks this device\'s conversations; country is decided by the server from the device\'s IP. |\n| **Frequency** | Remembered on the device: once per visitor, once per day, or once per app launch. |\n| Button **Open chat** / **Open a link** | Opens the chat / the link in the browser. |\n| Button **Open Knowledge Base** | Calls your `onOpenKnowledgeBase` (the chat has no built-in Knowledge Base screen); opens the chat if you don\'t pass it. |\n\n```tsx\n\n<KerAbieProvider\n widgetKey="wk_yourorg_xxx"\n screen={usePathname()}\n onOpenKnowledgeBase={() => router.push(\'/help\')}\n>\n```\n\nCampaign results (shown, opened, clicked, replied) are counted in the dashboard like they are for the website widget. Nothing pops up while the chat is open.\n\n## Notification setup\n\nKerabie push notifications deep-link directly to the conversation that triggered them.\n\n### Android \u2014 register channels first\n\n> **Android 8+ (API 26+) requirement:** Without a registered notification channel, push notifications are **silently dropped**. You must call `NotificationService.setup()` before the provider renders.\n\n```tsx\n// app/_layout.tsx or App.tsx \u2014 BEFORE KerAbieProvider renders\n\nuseEffect(() => {\n NotificationService.setup({\n projectId: \'your-expo-project-id\', // from app.json expo.extra.eas.projectId\n })\n}, [])\n```\n\nThis single call does everything:\n1. Registers Android notification channels (`kerabie_messages`, `kerabie_visitor`, `kerabie_system`)\n2. Requests OS permission from the user\n3. Gets the Expo push token\n4. Registers the token with the Kerabie API\n\n### Custom Android channels\n\nOverride the default channels to match your app branding:\n\n```tsx\n\nconst MY_CHANNELS: KerAndroidChannel[] = [\n {\n channelId: \'support_chat\',\n name: \'Support Messages\',\n importance: \'high\',\n sound: \'default\',\n enableVibrate: true,\n },\n]\n\nNotificationService.setup({ channels: MY_CHANNELS })\n```\n\n### app.json \u2014 required config\n\n```json\n{\n "expo": {\n "android": {\n "permissions": ["NOTIFICATIONS"],\n "googleServicesFile": "./google-services.json"\n },\n "plugins": [\n ["expo-notifications", {\n "icon": "./assets/notification-icon.png",\n "color": "#4e7969",\n "defaultChannel": "kerabie_messages"\n }]\n ]\n }\n}\n```\n\n### One-call setup\n\n```tsx\n\n// In your root layout, before the provider:\nuseEffect(() => {\n NotificationService.setup({\n projectId: \'your-expo-project-id\',\n onTokenReceived: (token) => console.log(\'Registered:\', token),\n })\n}, [])\n```\n\n### Manual setup (fine-grained control)\n\n```tsx\n\n// Set your global handler (module level, outside any component)\nNotifications.setNotificationHandler({\n handleNotification: async () => ({\n shouldShowAlert: true,\n shouldPlaySound: true,\n shouldSetBadge: true,\n }),\n})\n\n// Inside a component that is inside KerAbieProvider:\nfunction NotificationSetup() {\n const { requestPermission, registerToken } = useKerAbieNotifications()\n\n useEffect(() => {\n requestPermission().then(async (granted) => {\n if (!granted) return\n const { data: token } = await Notifications.getExpoPushTokenAsync()\n registerToken(token)\n })\n }, [])\n\n return null\n}\n```\n\n---\n\n## Contextual chat\n\nOpen the chat widget with a pre-loaded context so agents immediately see what the user needs help with \u2014 without the user having to explain it.\n\nUse `openWithContext` from `useKerAbieContext()` (or `useKerAbieChat()`). The context is sent to the Kerabie backend and shown to the agent in the conversation.\n\n### Product click\n\n```tsx\n\nfunction ProductCard({ product }) {\n const { openWithContext } = useKerAbieContext()\n\n return (\n <Pressable onPress={() => openWithContext({\n type: \'product\',\n id: String(product.id),\n title: product.name,\n data: {\n price: `\u20A6${product.price}`,\n category: product.category,\n sku: product.sku,\n },\n })}>\n <Text>Ask about this product</Text>\n </Pressable>\n )\n}\n```\n\nAgent sees: **"Product \u2014 Blue Sneakers"** with price, category, SKU.\n\n### Order inquiry\n\n```tsx\nconst { openWithContext } = useKerAbieContext()\n\n<Button onPress={() => openWithContext({\n type: \'order\',\n id: order.reference,\n title: `Order ${order.reference}`,\n data: {\n status: order.status,\n total: `\u20A6${order.total}`,\n placedAt: order.createdAt,\n items: order.itemCount,\n },\n})}>\n Get help with this order\n\n```\n\n### Fintech transaction\n\n```tsx\nopenWithContext({\n type: \'transaction\',\n id: txn.id,\n title: `\u20A6${txn.amount} ${txn.type}`,\n data: {\n status: txn.status, // "failed", "pending", "success"\n recipient: txn.recipientName,\n narration: txn.narration,\n reference: txn.reference,\n },\n})\n```\n\n### Ride sharing\n\n```tsx\nopenWithContext({\n type: \'ride\',\n id: ride.id,\n title: `Trip to ${ride.destination}`,\n data: {\n driver: ride.driverName,\n fare: `\u20A6${ride.fare}`,\n status: ride.status,\n startedAt: ride.startedAt,\n },\n})\n```\n\n### Context types\n\n| `type` | Best for |\n|---|---|\n| `product` | E-commerce \u2014 user asking about an item |\n| `order` | Order status, returns, refunds |\n| `transaction` | Fintech \u2014 failed or disputed payment |\n| `ride` | Ride-sharing \u2014 trip issue or complaint |\n| `errand` | Delivery or logistics issue |\n| `booking` | Appointment or reservation |\n| `custom` | Anything else \u2014 set a descriptive `title` |\n\n### Open without showing the chat\n\nPass `openChat: false` if you only want to set context (e.g. pre-load on screen mount) without immediately opening the widget:\n\n```tsx\nopenWithContext({\n type: \'order\',\n id: order.id,\n title: `Order ${order.reference}`,\n openChat: false, // context is stored; chat opens later when user taps the button\n})\n```\n\n---\n\n## User identity\n\nWhen a user logs in, call `setUser` so agents see who they\'re talking to:\n\n```tsx\nconst { setUser, clearUser, updateAttribute } = useKerAbieUser()\n\n// On login\nsetUser({\n id: currentUser.id, // your internal ID\n name: currentUser.name,\n email: currentUser.email,\n role: \'customer\',\n customAttributes: {\n plan: currentUser.plan,\n signedUpAt: currentUser.createdAt,\n },\n})\n\n// Add live context at any point (shows in agent sidebar immediately)\nupdateAttribute(\'cart_value\', \'\u20A612,500\')\nupdateAttribute(\'last_error\', \'CheckoutScreen: undefined is not an object\')\n\n// On logout \u2014 starts a fresh anonymous session\nclearUser()\n```\n\n---\n\n## Deep links \u2014 notification tap navigation\n\nWhen a user taps a push notification, the SDK opens the correct conversation automatically.\nThis works via the `kerabie://conversation/:id` URI scheme.\n\n### app.json \u2014 required config\n\n```json\n{\n "expo": {\n "scheme": "kerabie",\n "android": {\n "intentFilters": [\n {\n "action": "VIEW",\n "autoVerify": true,\n "data": [{ "scheme": "kerabie" }],\n "category": ["BROWSABLE", "DEFAULT"]\n }\n ]\n },\n "ios": {\n "bundleIdentifier": "com.yourcompany.yourapp",\n "associatedDomains": ["applinks:kerabie.com"]\n }\n }\n}\n```\n\n### How it works\n\nThe SDK registers a `Linking` listener automatically inside `KerAbieProvider`.\nWhen a notification is tapped:\n1. OS opens your app with the URL `kerabie://conversation/123`\n2. `KerAbieDeepLinkService` intercepts the URL\n3. The chat widget opens and scrolls to conversation 123\n\nNo extra setup needed \u2014 just add the `app.json` config above.\n\n### Custom scheme\n\nIf your app already uses a different scheme:\n\n```tsx\n// In the deep link handler init (you don\'t call this directly \u2014 \n// the provider handles it, but you can override via KerAbieDeepLinkService)\nKerAbieDeepLinkService.init({\n scheme: \'myapp\', // uses myapp://conversation/123 instead\n onConversation: (id) => { /* custom handler */ },\n})\n```\n\n### Send the deep link in Expo push notifications (backend)\n\nWhen the Kerabie backend sends an Expo push notification, it includes:\n\n```json\n{\n "to": "ExponentPushToken[...]",\n "title": "New message",\n "body": "You have a new message",\n "data": {\n "url": "kerabie://conversation/123",\n "conversationId": 123\n }\n}\n```\n\nThe `expo-notifications` handler in your app should call `Linking.openURL(notification.request.content.data.url)` on tap.\n\n---\n\n## License\n\nMIT\n\n## Building with an AI assistant\n\nThe [`@kerabie/mcp`](https://kerabie.com/docs/sdks/mcp) server gives Claude, Cursor and other MCP clients this package\'s docs and types, so they use the provider, hooks and components correctly instead of guessing. It needs no API key:\n\n```json\n{ "mcpServers": { "kerabie": { "command": "npx", "args": ["-y", "@kerabie/mcp"] } } }\n```\n\nAsk the assistant to use the `integrate_react_native` prompt, or the `get_integration_guide` and `get_sdk_types` tools.', headings: [{ level: 2, text: "Contents" }, { level: 2, text: "Installation" }, { level: 2, text: "Provider setup" }, { level: 3, text: "Expo Router (`app/_layout.tsx`)" }, { level: 3, text: "React Navigation (`App.tsx`)" }, { level: 2, text: "KerAbieProvider props" }, { level: 3, text: "Required" }, { level: 3, text: "App identity" }, { level: 3, text: "Open mode" }, { level: 3, text: "Floating button" }, { level: 3, text: "User identity" }, { level: 3, text: "Appearance" }, { level: 3, text: "Chat settings (override dashboard)" }, { level: 3, text: "Notifications" }, { level: 2, text: "Hooks" }, { level: 3, text: "useKerAbieChat" }, { level: 3, text: "useKerAbieUser" }, { level: 3, text: "useKerAbieApp" }, { level: 3, text: "useKerAbieNotifications" }, { level: 2, text: "Components" }, { level: 3, text: "KerAbieFloatingButton" }, { level: 3, text: "KerAbieChatScreen" }, { level: 2, text: "Types" }, { level: 2, text: "Open modes in depth" }, { level: 3, text: '`openMode="modal"` \u2014 recommended default' }, { level: 3, text: '`openMode="sheet"` \u2014 bottom sheet' }, { level: 3, text: '`openMode="push"` \u2014 navigation stack' }, { level: 2, text: "Campaigns" }, { level: 2, text: "Notification setup" }, { level: 3, text: "Android \u2014 register channels first" }, { level: 3, text: "Custom Android channels" }, { level: 3, text: "app.json \u2014 required config" }, { level: 3, text: "One-call setup" }, { level: 3, text: "Manual setup (fine-grained control)" }, { level: 2, text: "Contextual chat" }, { level: 3, text: "Product click" }, { level: 3, text: "Order inquiry" }, { level: 3, text: "Fintech transaction" }, { level: 3, text: "Ride sharing" }, { level: 3, text: "Context types" }, { level: 3, text: "Open without showing the chat" }, { level: 2, text: "User identity" }, { level: 2, text: "Deep links \u2014 notification tap navigation" }, { level: 3, text: "app.json \u2014 required config" }, { level: 3, text: "How it works" }, { level: 3, text: "Custom scheme" }, { level: 3, text: "Send the deep link in Expo push notifications (backend)" }, { level: 2, text: "License" }, { level: 2, text: "Building with an AI assistant" }] }, { id: "readme/chat-widget", url: "", title: "Chat widget README", description: "The Chat widget package readme, straight from its repository.", section: "widget", body: '# Vue 3 + Vite\n\nThis template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.\n\nLearn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).\n"# kerabie-widget"', headings: [] }], endpoints: [{ method: "GET", path: "/agents", title: "List agents", doc: "api-reference/agents", sdk: "" }, { method: "GET", path: "/agents/:id", title: "Get an agent", doc: "api-reference/agents", sdk: "" }, { method: "GET", path: "/v1/ai-tools", title: "List tools", doc: "api-reference/ai-tools", sdk: "kerabie.aiTools.list(...)" }, { method: "GET", path: "/v1/ai-tools/:id", title: "Get a tool", doc: "api-reference/ai-tools", sdk: "" }, { method: "POST", path: "/v1/ai-tools", title: "Create a tool", doc: "api-reference/ai-tools", sdk: "kerabie.aiTools.create(...)" }, { method: "PATCH", path: "/v1/ai-tools/:id", title: "Update a tool", doc: "api-reference/ai-tools", sdk: "kerabie.aiTools.update(...)" }, { method: "POST", path: "/v1/ai-tools/:id/duplicate", title: "Duplicate a tool", doc: "api-reference/ai-tools", sdk: "" }, { method: "DELETE", path: "/v1/ai-tools/:id", title: "Delete a tool", doc: "api-reference/ai-tools", sdk: "" }, { method: "POST", path: "/v1/ai-tools/:id/test", title: "Test a tool", doc: "api-reference/ai-tools", sdk: "kerabie.aiTools.test(...)" }, { method: "GET", path: "/v1/analytics/summary", title: "Summary", doc: "api-reference/analytics", sdk: "kerabie.analytics.getSummary(...)" }, { method: "GET", path: "/v1/analytics/insights", title: "Insights", doc: "api-reference/analytics", sdk: "kerabie.analytics.getInsights(...)" }, { method: "GET", path: "/v1/analytics/agents", title: "Agent performance", doc: "api-reference/analytics", sdk: "kerabie.analytics.getAgentStats(...)" }, { method: "GET", path: "/v1/analytics/tickets-sla", title: "Open tickets and SLA", doc: "api-reference/analytics", sdk: "kerabie.analytics.getTicketSla(...)" }, { method: "GET", path: "/v1/analytics/setup-status", title: "Setup status", doc: "api-reference/analytics", sdk: "kerabie.analytics.getSetupStatus(...)" }, { method: "GET", path: "/v1/campaigns", title: "List campaigns", doc: "api-reference/campaigns", sdk: "kerabie.campaigns.list(...)" }, { method: "GET", path: "/v1/campaigns/stats", title: "Overview numbers", doc: "api-reference/campaigns", sdk: "" }, { method: "GET", path: "/v1/campaigns/:id", title: "Get a campaign", doc: "api-reference/campaigns", sdk: "" }, { method: "POST", path: "/v1/campaigns", title: "Create a campaign", doc: "api-reference/campaigns", sdk: "kerabie.campaigns.create(...)" }, { method: "PATCH", path: "/v1/campaigns/:id", title: "Update a campaign", doc: "api-reference/campaigns", sdk: "" }, { method: "POST", path: "/v1/campaigns/:id/duplicate", title: "Duplicate a campaign", doc: "api-reference/campaigns", sdk: "" }, { method: "DELETE", path: "/v1/campaigns/:id", title: "Delete a campaign", doc: "api-reference/campaigns", sdk: "" }, { method: "GET", path: "/v1/contacts", title: "List contacts", doc: "api-reference/contacts", sdk: "kerabie.contacts.list(...)" }, { method: "GET", path: "/v1/contacts/stats", title: "Overview numbers", doc: "api-reference/contacts", sdk: "" }, { method: "GET", path: "/v1/contacts/:id", title: "Get a contact", doc: "api-reference/contacts", sdk: "" }, { method: "POST", path: "/v1/contacts", title: "Add a contact", doc: "api-reference/contacts", sdk: "kerabie.contacts.create(...)" }, { method: "PATCH", path: "/v1/contacts/:id", title: "Update a contact", doc: "api-reference/contacts", sdk: "" }, { method: "POST", path: "/v1/contacts/import", title: "Import contacts", doc: "api-reference/contacts", sdk: "kerabie.contacts.import(...)" }, { method: "GET", path: "/v1/conversations", title: "List conversations", doc: "api-reference/conversations", sdk: "kerabie.conversations.list(...)" }, { method: "GET", path: "/v1/conversations/:id", title: "Get a conversation", doc: "api-reference/conversations", sdk: "kerabie.conversations.get(...)" }, { method: "GET", path: "/v1/conversations/:id/history", title: "Previous conversations", doc: "api-reference/conversations", sdk: "kerabie.conversations.getHistory(...)" }, { method: "POST", path: "/v1/conversations/:id/take-over", title: "Take over from the AI", doc: "api-reference/conversations", sdk: "kerabie.conversations.takeOver(...)" }, { method: "POST", path: "/conversations/:id/resolve", title: "Resolve a conversation", doc: "api-reference/conversations", sdk: "" }, { method: "POST", path: "/conversations/:id/close", title: "Close a conversation", doc: "api-reference/conversations", sdk: "" }, { method: "PATCH", path: "/conversations/:id", title: "Assign to an agent", doc: "api-reference/conversations", sdk: "" }, { method: "POST", path: "/conversations/:id/messages", title: "Send a message", doc: "api-reference/conversations", sdk: "" }, { method: "GET", path: "/customers", title: "List customers", doc: "api-reference/customers", sdk: "" }, { method: "GET", path: "/customers/:id", title: "Get a customer", doc: "api-reference/customers", sdk: "" }, { method: "GET", path: "/customers/by-email/:email", title: "Get by email", doc: "api-reference/customers", sdk: "" }, { method: "PATCH", path: "/customers/:id", title: "Update a customer", doc: "api-reference/customers", sdk: "" }, { method: "DELETE", path: "/customers/:id", title: "Delete a customer (GDPR)", doc: "api-reference/customers", sdk: "" }, { method: "POST", path: "/growth/utm/visit", title: "UTM Attribution", doc: "api-reference/growth", sdk: "" }, { method: "GET", path: "/growth/utm/analytics?days=30", title: "UTM Attribution", doc: "api-reference/growth", sdk: "" }, { method: "GET", path: "/growth/utm/timeline?days=30", title: "UTM Attribution", doc: "api-reference/growth", sdk: "" }, { method: "POST", path: "/partner/apply", title: "Partner Program", doc: "api-reference/growth", sdk: "" }, { method: "GET", path: "/partner/me", title: "Partner Program", doc: "api-reference/growth", sdk: "" }, { method: "GET", path: "/partner/earnings?days=30", title: "Partner Program", doc: "api-reference/growth", sdk: "" }, { method: "GET", path: "/partner/verify/{code}", title: "Partner Program", doc: "api-reference/growth", sdk: "" }, { method: "POST", path: "/partner/payout/request", title: "Partner Program", doc: "api-reference/growth", sdk: "" }, { method: "GET", path: "/v1/kb/articles", title: "Authenticated API (`/v1/kb`)", doc: "api-reference/knowledge-base", sdk: "" }, { method: "GET", path: "/v1/kb/stats", title: "Authenticated API (`/v1/kb`)", doc: "api-reference/knowledge-base", sdk: "" }, { method: "GET", path: "/v1/kb/insights", title: "Authenticated API (`/v1/kb`)", doc: "api-reference/knowledge-base", sdk: "" }, { method: "GET", path: "/v1/kb/articles/:id", title: "Authenticated API (`/v1/kb`)", doc: "api-reference/knowledge-base", sdk: "" }, { method: "POST", path: "/v1/kb/articles", title: "Authenticated API (`/v1/kb`)", doc: "api-reference/knowledge-base", sdk: "" }, { method: "PATCH", path: "/v1/kb/articles/:id", title: "Authenticated API (`/v1/kb`)", doc: "api-reference/knowledge-base", sdk: "" }, { method: "DELETE", path: "/v1/kb/articles/:id", title: "Authenticated API (`/v1/kb`)", doc: "api-reference/knowledge-base", sdk: "" }, { method: "GET", path: "/public/kb/articles?widgetKey=wk_yourorg_abc123&search=refund", title: "Public, widget-facing API (`/public/kb`)", doc: "api-reference/knowledge-base", sdk: "" }, { method: "GET", path: "/public/kb/articles/:id?widgetKey=wk_yourorg_abc123", title: "Public, widget-facing API (`/public/kb`)", doc: "api-reference/knowledge-base", sdk: "" }, { method: "POST", path: "/public/kb/articles/:id/feedback?widgetKey=wk_yourorg_abc123", title: "Public, widget-facing API (`/public/kb`)", doc: "api-reference/knowledge-base", sdk: "" }, { method: "POST", path: "/knowledge-base/import/document", title: "Importing content", doc: "api-reference/knowledge-base", sdk: "" }, { method: "POST", path: "/knowledge-base/crawl/discover", title: "Importing content", doc: "api-reference/knowledge-base", sdk: "" }, { method: "POST", path: "/knowledge-base/crawl/import", title: "Importing content", doc: "api-reference/knowledge-base", sdk: "" }, { method: "GET", path: "/knowledge-base/jobs/:jobId", title: "Importing content", doc: "api-reference/knowledge-base", sdk: "" }, { method: "POST", path: "/knowledge-base/:id/refine", title: "Importing content", doc: "api-reference/knowledge-base", sdk: "" }, { method: "GET", path: "/store/products", title: "Products", doc: "api-reference/store", sdk: "" }, { method: "POST", path: "/store/products", title: "Products", doc: "api-reference/store", sdk: "" }, { method: "GET", path: "/store/products/stats", title: "Products", doc: "api-reference/store", sdk: "" }, { method: "POST", path: "/public/store/{slug}/view/{productId}", title: "Public Storefront", doc: "api-reference/store", sdk: "" }, { method: "POST", path: "/public/store/{slug}/inquiry/{productId}", title: "Public Storefront", doc: "api-reference/store", sdk: "" }, { method: "GET", path: "/store/orders?status=pending", title: "Orders", doc: "api-reference/store", sdk: "" }, { method: "PATCH", path: "/store/orders/{id}", title: "Orders", doc: "api-reference/store", sdk: "" }, { method: "GET", path: "/v1/tickets/stats", title: "Overview numbers", doc: "api-reference/tickets", sdk: "kerabie.tickets.getStats(...)" }, { method: "GET", path: "/v1/tickets/export", title: "Export to CSV", doc: "api-reference/tickets", sdk: "kerabie.tickets.export(...)" }, { method: "GET", path: "/v1/visitors", title: "List visitors", doc: "api-reference/visitors", sdk: "kerabie.visitors.list(...)" }, { method: "GET", path: "/v1/visitors/overview", title: "Overview numbers", doc: "api-reference/visitors", sdk: "kerabie.visitors.getOverview(...)" }, { method: "GET", path: "/v1/visitors/export", title: "Export to CSV", doc: "api-reference/visitors", sdk: "kerabie.visitors.export(...)" }, { method: "GET", path: "/webhooks", title: "List webhooks", doc: "api-reference/webhooks", sdk: "" }, { method: "POST", path: "/webhooks", title: "Create a webhook", doc: "api-reference/webhooks", sdk: "" }, { method: "PATCH", path: "/webhooks/:id", title: "Update a webhook", doc: "api-reference/webhooks", sdk: "" }, { method: "DELETE", path: "/webhooks/:id", title: "Delete a webhook", doc: "api-reference/webhooks", sdk: "" }, { method: "GET", path: "/webhooks/:id/deliveries", title: "List delivery logs", doc: "api-reference/webhooks", sdk: "" }, { method: "POST", path: "/webhooks/:id/deliveries/:deliveryId/retry", title: "Retry a delivery", doc: "api-reference/webhooks", sdk: "" }], types: { typescript: "interface KerAbieClientConfig {\n apiKey: string;\n orgId?: string;\n baseUrl?: string;\n timeout?: number;\n}\ninterface PaginationMeta {\n total: number;\n page: number;\n limit: number;\n hasMore?: boolean;\n /** Total pages (conversation lists). */\n pages?: number;\n /** Organisation-wide totals per status \u2014 ignores the list filters (conversation lists). */\n counts?: ConversationCounts;\n}\ntype ConversationStatus = 'WAITING' | 'ACTIVE' | 'RESOLVED' | 'CLOSED';\ntype ConversationChannel = 'WEBSITE' | 'WHATSAPP' | 'INSTAGRAM' | 'EMAIL' | 'API';\ninterface Conversation {\n id: number;\n status: ConversationStatus;\n channel: ConversationChannel;\n subject?: string | null;\n customerId?: number | null;\n customer?: {\n id: number;\n name: string | null;\n email: string | null;\n sessionId: string;\n };\n /** Team-member id of the assignee, if any. */\n assignedTeamMemberId?: number | null;\n assignedAgent?: {\n id: number;\n userId: number;\n user: {\n fname: string | null;\n lname: string | null;\n email: string;\n avatar?: string | null;\n };\n } | null;\n rating?: number | null;\n ratingComment?: string | null;\n /** Text of the most recent message (list responses). */\n lastMessage?: string | null;\n lastMessageAt?: string | null;\n /** First few tags (list responses); `get` returns all of them. */\n tags?: {\n tag: string;\n }[];\n /** True while the AI bot is handling the conversation alone: still open, no human reply, not handed off. */\n aiHandling?: boolean;\n /** AI recap written when the bot hands off to a human. */\n handoffSummary?: string | null;\n /** Unread message count, under `_count.messages` (list responses). */\n _count?: {\n messages: number;\n };\n createdAt: string;\n updatedAt: string;\n}\n/** Organisation-wide conversation totals per status. */\ninterface ConversationCounts {\n all: number;\n WAITING: number;\n ACTIVE: number;\n RESOLVED: number;\n CLOSED: number;\n}\n/** An earlier conversation with the same customer. */\ninterface PreviousConversation {\n id: number;\n status: ConversationStatus;\n channel: ConversationChannel;\n subject: string | null;\n isTicket: boolean;\n createdAt: string;\n lastMessageAt: string | null;\n handoffSummary: string | null;\n}\ninterface TakeOverParams {\n /** Organisation team-member id the conversation is assigned to if nobody holds it yet. */\n agentId: number;\n}\ninterface ListConversationsParams {\n status?: ConversationStatus;\n channel?: ConversationChannel;\n page?: number;\n limit?: number;\n search?: string;\n assignedTo?: number;\n}\ninterface CreateConversationParams {\n customerId?: number;\n customerEmail?: string;\n subject?: string;\n channel?: ConversationChannel;\n metadata?: Record<string, any>;\n}\ninterface UpdateConversationParams {\n status?: ConversationStatus;\n assignedTeamMemberId?: number;\n rating?: number;\n ratingComment?: string;\n}\ntype MessageType = 'text' | 'image' | 'file' | 'audio' | 'video' | 'system';\ntype SenderType = 'agent' | 'visitor' | 'bot';\ninterface Message {\n id: number;\n conversationId: number;\n body: string;\n type: MessageType;\n senderType: SenderType;\n senderName?: string;\n agentId?: number;\n metadata?: Record<string, any>;\n attachments?: Attachment[];\n createdAt: string;\n}\ninterface Attachment {\n id: string;\n url: string;\n type: 'image' | 'file' | 'audio' | 'video';\n name?: string;\n size?: number;\n mimeType?: string;\n}\ninterface SendMessageParams {\n body: string;\n type?: MessageType;\n metadata?: Record<string, any>;\n}\ninterface ListMessagesParams {\n limit?: number;\n before?: number;\n}\ninterface Customer {\n id: number;\n name?: string;\n email?: string;\n phone?: string;\n avatar?: string;\n externalSource?: string;\n externalRef?: string;\n customAttributes?: Record<string, string>;\n conversationCount?: number;\n createdAt: string;\n updatedAt: string;\n}\ninterface UpsertCustomerParams {\n email?: string;\n name?: string;\n phone?: string;\n externalSource?: string;\n externalRef?: string;\n customAttributes?: Record<string, string>;\n}\ntype VisitorStatus = 'online' | 'offline' | 'blocked' | 'allowed';\n/** A visitor from the last few days \u2014 on the site now or recently gone. */\ninterface Visitor {\n /** The widget's anonymous session id. */\n sessionId: string;\n /** From the contact record if they have chatted, otherwise as identified to the widget. */\n name: string | null;\n email: string | null;\n phone: string | null;\n /** ISO 3166-1 alpha-2 code, e.g. \"NG\". */\n country: string | null;\n city: string | null;\n /** null when the organisation hides visitor IPs in its privacy settings. */\n ip: string | null;\n userAgent: string | null;\n /** The page they are on now (online) or were last on. */\n currentPage: string | null;\n /** Visits so far. Coming back after 30+ minutes away counts as a new visit. */\n visitCount: number;\n firstSeenAt: string;\n lastSeenAt: string;\n /** Their most recent conversation, if they have chatted. */\n conversationId: number | null;\n /** Blocked / allowed come from your IP rules and win over online / offline. */\n status: VisitorStatus;\n}\ninterface ListVisitorsParams {\n status?: 'all' | VisitorStatus;\n /** Matches name, email, IP, city or page, case-insensitively. */\n search?: string;\n /** Two-letter country code. */\n country?: string;\n /** How far back to look: default 7, max 30 (visitor history is kept for 30 days). */\n days?: number;\n page?: number;\n limit?: number;\n}\ninterface VisitorsOverview {\n /** On the site right now. */\n online: number;\n /** Distinct countries in the last 24 hours. */\n countries: number;\n /** Visitors in the last 24 hours who have visited more than once. */\n returning: number;\n /** Active IP block rules. */\n blockedIps: number;\n topCountries: {\n country: string;\n count: number;\n }[];\n}\ninterface VisitorsExport {\n /** e.g. visitors-2026-10-05.csv */\n filename: string;\n csv: string;\n count: number;\n /** True when more than 5,000 matched and the file was cut at 5,000. */\n truncated: boolean;\n}\ninterface BlockVisitorParams {\n reason?: string;\n blockIp?: boolean;\n}\ntype CampaignStatus = 'active' | 'paused' | 'draft';\ntype CampaignTrigger = 'page_load' | 'time_on_page' | 'scroll_depth' | 'exit_intent';\ntype CampaignAction = 'open_chat' | 'open_kb' | 'open_link';\ntype CampaignAudience = 'all' | 'new' | 'returning' | 'not_chatted' | 'country';\ntype CampaignFrequency = 'once_per_visitor' | 'once_per_day' | 'every_session';\ninterface Campaign {\n id: number;\n name: string;\n /** What the visitor sees. */\n message: string;\n /** Button text; no button when null. */\n ctaLabel: string | null;\n ctaAction: CampaignAction;\n /** Only for `open_link`. */\n ctaUrl: string | null;\n trigger: CampaignTrigger;\n /** Seconds for `time_on_page`, percent for `scroll_depth`, otherwise null. */\n triggerValue: string | null;\n /** Comma-separated URL patterns (`*` wildcards); null = every page. */\n pages: string | null;\n audience: CampaignAudience;\n /** ISO country code (e.g. `NG`), only for audience `country`. */\n audienceCountry: string | null;\n frequency: CampaignFrequency;\n isActive: boolean;\n /** `draft` = never launched, `paused` = launched before but off. */\n status: CampaignStatus;\n launchedAt: string | null;\n /** Times it was shown to a visitor. */\n sentCount: number;\n /** Visitors who opened the chat from it. */\n openedCount: number;\n /** Visitors who pressed its button. */\n clickCount: number;\n /** Visitors who wrote back after opening it. */\n repliedCount: number;\n createdAt: string;\n}\ninterface ListCampaignsParams {\n status?: CampaignStatus;\n /** Matches name or message, case-insensitively. */\n search?: string;\n page?: number;\n limit?: number;\n}\ninterface CreateCampaignParams {\n name: string;\n message: string;\n ctaLabel?: string;\n ctaAction?: CampaignAction;\n ctaUrl?: string;\n trigger?: CampaignTrigger;\n triggerValue?: string | number;\n pages?: string;\n audience?: CampaignAudience;\n /** Required when audience is `country`. */\n audienceCountry?: string;\n frequency?: CampaignFrequency;\n /** `true` launches it straight away (counts against your plan's live-campaign limit). Default: saved as a draft. */\n isActive?: boolean;\n}\ntype UpdateCampaignParams = Partial<CreateCampaignParams>;\ninterface CampaignStats {\n total: number;\n active: number;\n paused: number;\n draft: number;\n sent: number;\n opened: number;\n clicked: number;\n replied: number;\n /** Most campaigns your plan lets you run at once; 0 = unlimited. */\n limit: number;\n}\ntype ContactType = 'LEAD' | 'CUSTOMER' | 'VIP';\ninterface Contact {\n id: number;\n name: string | null;\n email: string | null;\n phone: string | null;\n company: string | null;\n notes: string | null;\n contactType: ContactType;\n /** Team-member id of the owner, if any. */\n ownerId: number | null;\n owner: {\n id: number;\n user: {\n fname: string | null;\n lname: string | null;\n email: string;\n avatar?: string | null;\n };\n } | null;\n tagLinks: {\n tag: {\n id: number;\n name: string;\n color: string | null;\n };\n }[];\n /** Two-letter country code, from the visitor's IP when they chatted in. */\n visitorCountry: string | null;\n /** 'manual', 'import', or null for contacts who wrote in themselves. */\n externalSource: string | null;\n /** Last message in either direction; falls back to the latest conversation for older contacts. */\n lastContactAt: string | null;\n createdAt: string;\n _count: {\n conversations: number;\n };\n}\ninterface ListContactsParams {\n /** Matches name, email, phone, company or a tag name, case-insensitively. */\n search?: string;\n type?: ContactType;\n tagId?: number;\n /** Team-member id. */\n ownerId?: number;\n /** 'recent' (default) = last contact first, contacts never messaged last. */\n sort?: 'recent' | 'newest' | 'name';\n page?: number;\n limit?: number;\n}\ninterface CreateContactParams {\n name?: string;\n email?: string;\n phone?: string;\n company?: string;\n notes?: string;\n contactType?: ContactType;\n ownerId?: number | null;\n}\ntype UpdateContactParams = CreateContactParams;\ninterface ContactStats {\n total: number;\n /** Added in the last 7 days. */\n newThisWeek: number;\n leads: number;\n vip: number;\n}\ninterface ImportContactRow {\n name?: string;\n email?: string;\n phone?: string;\n company?: string;\n /** lead, customer or vip (any case). Defaults to lead. */\n type?: string;\n}\ninterface ImportContactsResult {\n created: number;\n /** Rows skipped because the email already belongs to a contact. */\n duplicates: number;\n /** Rows that could not be imported. `row` is 1-based, not counting the header. */\n invalid: {\n row: number;\n error: string;\n }[];\n total: number;\n}\ninterface Agent {\n id: number;\n fname: string;\n lname: string;\n email: string;\n avatar?: string;\n availability?: 'online' | 'away' | 'offline';\n role?: string;\n}\ninterface InviteAgentParams {\n email: string;\n accessLevel?: 'ADMIN' | 'CHAT_ONLY' | 'READ_ONLY';\n teamId?: number;\n}\ntype KbArticleType = 'faq' | 'article' | 'news' | 'information';\ntype KbArticleStatus = 'draft' | 'published' | 'archived';\ninterface KbArticle {\n id: number;\n title: string;\n content: string;\n category: string;\n type: KbArticleType;\n status: KbArticleStatus;\n imageUrl?: string;\n views: number;\n /** Share of widget visitors who found it helpful (0-100); null until at least 3 have voted. */\n helpfulPct: number | null;\n /** How many visitors have voted. */\n votes: number;\n /** Times the AI chatbot cited it in an answer. */\n aiCitations: number;\n createdAt: string;\n updatedAt: string;\n}\ninterface KbStats {\n total: number;\n published: number;\n drafts: number;\n views: number;\n /** The most viewed published article; null until something has been viewed. */\n top: {\n title: string;\n views: number;\n } | null;\n /** Categories in use. */\n categories: string[];\n}\ninterface KbInsights {\n mostViewed: {\n id: number;\n title: string;\n views: number;\n pct: number;\n }[];\n /** Customer questions the AI had no article to answer from in the last 30 days: what to write next. */\n gaps: {\n question: string;\n count: number;\n }[];\n /** Published articles with the lowest helpfulness, lowest first (needs 3 votes). */\n lowestRated: {\n id: number;\n title: string;\n helpfulPct: number;\n votes: number;\n }[];\n /** Published articles not updated in 90 days. */\n stale: {\n id: number;\n title: string;\n age: string;\n }[];\n}\ninterface CreateArticleParams {\n title: string;\n content: string;\n category?: string;\n type?: KbArticleType;\n status?: KbArticleStatus;\n}\ntype TicketPriority = 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT';\ninterface Ticket {\n id: number;\n status: ConversationStatus;\n priority: TicketPriority;\n subject?: string;\n slaDueAt?: string;\n customerId?: number;\n createdAt: string;\n updatedAt: string;\n}\n/** KPI numbers for the tickets overview. */\ninterface TicketStats {\n /** Tickets that are new or active. */\n open: number;\n /** Open tickets nobody owns. */\n unassigned: number;\n /** Open tickets past their SLA due time. */\n breachingSla: number;\n /** Average open\u2192resolved time in minutes over the last 7 days; null when none were resolved. */\n avgResolutionMinutes: number | null;\n /** % change vs the 7 days before; negative = faster. Null when there is nothing to compare. */\n resolutionDeltaPct: number | null;\n}\ninterface ExportTicketsParams {\n status?: ConversationStatus;\n priority?: TicketPriority;\n /** Matches requester name or email, subject, or a ticket reference such as KB-2041. */\n search?: string;\n /** User id of the assigned agent. */\n assignedTo?: number;\n}\ninterface TicketExport {\n /** e.g. tickets-2026-10-05.csv */\n filename: string;\n csv: string;\n count: number;\n /** True when more than 5,000 tickets matched and the file was cut at 5,000. */\n truncated: boolean;\n}\ninterface CreateTicketParams {\n /** A stable identifier for this end user in your own system \u2014 repeated calls with the same externalRef land on the same ticket. */\n externalRef: string;\n message: string;\n subject?: string;\n priority?: TicketPriority;\n name?: string;\n email?: string;\n}\ninterface UpdateTicketParams {\n status?: ConversationStatus;\n priority?: TicketPriority;\n}\ninterface TicketMessage {\n id: number;\n body: string;\n senderRole: 'agent' | 'customer';\n senderName?: string;\n createdAt: string;\n}\ninterface StoreProduct {\n id: number;\n name: string;\n description?: string;\n price: number;\n currency: string;\n imageUrl?: string;\n category?: string;\n stock?: number;\n sku?: string;\n isActive: boolean;\n createdAt: string;\n updatedAt: string;\n}\ntype WebhookEvent = 'message.sent.customer' | 'message.sent.agent' | 'conversation.created' | 'conversation.resolved' | 'conversation.assigned' | 'visitor.new' | 'rating.submitted';\ninterface Webhook {\n id: number;\n url: string;\n events: WebhookEvent[];\n isActive: boolean;\n secret?: string;\n createdAt: string;\n}\ninterface CreateWebhookParams {\n url: string;\n events: WebhookEvent[];\n secret?: string;\n}\ninterface AnalyticsSummary {\n period: {\n days: number;\n since: string;\n };\n /** Currently open (new + active) conversations. */\n openConversations: number;\n /** Conversations resolved or closed since midnight. */\n resolvedToday: number;\n /** Average first-response time in seconds (0 when there is no data). */\n avgResponseTime: number;\n /** Customer satisfaction 0\u2013100 (average rating \xD7 20). */\n satisfactionScore: number;\n /** Visitors on the website right now. */\n activeVisitors: number;\n totals: {\n conversations: number;\n resolved: number;\n closed: number;\n messages: number;\n visitors: number;\n aiChats: number;\n /** Tickets created in the period. */\n tickets: number;\n /** Contacts added (someone with a name, email or phone). */\n contacts: number;\n /** Website visitor sessions that started in the period (history is kept 30 days, so longer windows count that far back). */\n siteVisitors: number;\n };\n rates: {\n resolution: number;\n };\n satisfaction: {\n avgRating: number | null;\n totalRatings: number;\n };\n /** % change vs the previous period of the same length; null = nothing to compare. For responseTime, negative is an improvement. */\n deltas: {\n conversations: number | null;\n resolved: number | null;\n responseTime: number | null;\n satisfaction: number | null;\n aiChats: number | null;\n };\n /** The AI assistant's share of the work. */\n ai: {\n /** Conversations the AI handled alone (no human ever replied). */\n resolvedAlone: number;\n resolvedAlonePct: number;\n /** Share of conversations handed off to a human. */\n handedOffPct: number;\n /** Estimate: AI-only conversations \xD7 the average time agents spent on human-handled ones. */\n agentHoursSaved: number;\n };\n channels: {\n channel: string;\n count: number;\n }[];\n aiEscalations: {\n reason: string;\n count: number;\n }[];\n /** Daily buckets (up to 30), oldest first. `ai` + `human` = `count`. */\n trend: {\n date: string;\n count: number;\n ai: number;\n human: number;\n }[];\n}\ninterface AnalyticsParams {\n /** Window in days (default 30, max 365). */\n days?: number;\n /** Alternative to `days`, e.g. '7d' or '30d'. */\n period?: string;\n}\ninterface AgentStat {\n agentId: number;\n /** Full name, falling back to the email's local part for agents who haven't set one. */\n name: string;\n avatar?: string | null;\n availability: 'online' | 'away' | 'busy' | 'offline' | string;\n messages: number;\n assigned: number;\n resolved: number;\n resolutionRate: number;\n avgRating: number | null;\n}\n/** An agent in {@link AnalyticsInsights}: the usual stats plus first-response time. */\ninterface AgentInsight extends AgentStat {\n /** Average time to this agent's first reply on conversations they answered first; null when there are none. */\n firstResponseSeconds: number | null;\n}\ninterface QuestionInsight {\n /** The wording customers used most. */\n question: string;\n asked: number;\n /** Share of these conversations the AI handled without a person stepping in. */\n aiResolvedPct: number;\n /** Average customer rating (1-5) of these conversations; null when none were rated. */\n avgRating: number | null;\n /** The page most of them started on. */\n page: string | null;\n /** % change in how often it was asked vs the previous period; null when it is new. */\n trendPct: number | null;\n}\n/** The deeper numbers behind the dashboard's Analytics page. */\ninterface AnalyticsInsights {\n period: {\n days: number;\n since: string;\n };\n /** Median time from a customer's first message to a person's first reply. `deltaPct` is vs the previous period (negative = faster). */\n firstResponse: {\n medianSeconds: number | null;\n deltaPct: number | null;\n sample: number;\n };\n agents: AgentInsight[];\n channels: {\n channel: string;\n count: number;\n pct: number;\n firstResponseSeconds: number | null;\n avgRating: number | null;\n }[];\n ai: {\n csatOnAiChats: {\n avg: number | null;\n count: number;\n };\n /** The most frequently asked questions (top 20), grouped by wording. */\n questions: QuestionInsight[];\n /** Questions handed to a person because nothing in the knowledge base matched. */\n unanswered: {\n question: string;\n count: number;\n }[];\n outcomes: {\n resolvedByAi: number;\n handedOff: number;\n other: number;\n };\n };\n customers: {\n active: number;\n new: number;\n returning: number;\n perCustomer: number | null;\n deltas: {\n active: number | null;\n new: number | null;\n returning: number | null;\n };\n /** One row per star, 5 first. */\n csat: {\n stars: number;\n count: number;\n pct: number;\n }[];\n csatResponses: number;\n countries: {\n country: string;\n count: number;\n }[];\n newPct: number;\n returningPct: number;\n top: {\n id: number;\n name: string;\n company: string | null;\n conversations: number;\n lastSeenAt: string | null;\n avgRating: number | null;\n }[];\n /** Customer messages by weekday (Monday first) and two-hour block (0, 2, \u2026 22), in the organisation's timezone. */\n heatmap: {\n days: string[];\n cells: number[][];\n timezone: string;\n };\n };\n /** Pages that start the most conversations. `askRate` is null until the page has recorded views. */\n pages: {\n page: string;\n views: number;\n questions: number;\n askRate: number | null;\n topQuestion: string | null;\n }[];\n}\n/** Open tickets by priority and how many are still inside their SLA. */\ninterface TicketSla {\n total: number;\n withinSlaPct: number;\n byPriority: {\n priority: 'URGENT' | 'HIGH' | 'NORMAL' | 'LOW';\n count: number;\n oldestMinutes: number | null;\n }[];\n}\n/** Which first-run steps are done. */\ninterface SetupStatus {\n channel: boolean;\n ai: boolean;\n automation: boolean;\n}\ninterface WebhookPayload<T = any> {\n event: WebhookEvent;\n orgId: number;\n timestamp: string;\n data: T;\n}\ntype AiToolMethod = 'GET' | 'POST' | 'PUT' | 'PATCH';\ntype AiToolAuthType = 'none' | 'bearer' | 'api_key' | 'basic';\ninterface AiToolParameter {\n /** Letters, numbers and underscores. */\n name: string;\n type: 'string' | 'number' | 'boolean';\n /** What the AI should put here, e.g. \"Product SKU the customer mentioned\". */\n description?: string;\n /** Default `true`: the AI collects it before calling your API. */\n required?: boolean;\n}\ninterface AiToolUsage {\n /** Calls in the last 30 days. */\n calls: number;\n /** Calls that failed (error or HTTP 4xx/5xx) in the last 30 days. */\n failures: number;\n lastUsedAt: string | null;\n}\ninterface AiTool {\n id: number;\n /** The snake_case function name the AI calls. */\n name: string;\n displayName: string;\n /** The AI reads this to decide when to call the tool. */\n description: string;\n parameters: Required<AiToolParameter>[];\n endpoint: string;\n method: AiToolMethod;\n authType: AiToolAuthType;\n /** Dot-notation path into the JSON response; null = the whole response. */\n responsePath: string | null;\n enabled: boolean;\n createdAt: string;\n updatedAt: string;\n usage: AiToolUsage;\n}\n/** One tool as returned by `get`: secrets are never returned, `hasSecret` says whether one is stored. */\ninterface AiToolDetail extends Omit<AiTool, 'usage'> {\n /** Names and headers only; secret values come back empty. */\n authConfig: Record<string, string> | null;\n headers: Record<string, string> | null;\n hasSecret: boolean;\n}\ninterface AiToolStats {\n total: number;\n active: number;\n /** Calls in the last 30 days. */\n calls: number;\n /** Percent of calls that succeeded; null when there were none. */\n successRate: number | null;\n /** How many tools your plan allows: -1 = not on your plan, 0 = unlimited. */\n limit: number;\n}\ninterface CreateAiToolParams {\n /** snake_case, unique in your organisation. */\n name: string;\n displayName: string;\n /** 10-500 characters. Say when the AI should use the tool. */\n description: string;\n parameters?: AiToolParameter[];\n /** A public https:// or http:// address. Private and local addresses are rejected. */\n endpoint: string;\n /** Default `POST`. GET sends parameters in the query string, the others as a JSON body. */\n method?: AiToolMethod;\n /** Default `none`. */\n authType?: AiToolAuthType;\n /** bearer: `{ token }` | api_key: `{ header, value }` | basic: `{ username, password }`. Write-only. */\n authConfig?: Record<string, string>;\n /** Extra static headers sent on every call (10 at most). */\n headers?: Record<string, string>;\n responsePath?: string;\n /** Default `true`. */\n enabled?: boolean;\n}\n/** Send only what you want to change. Leave a secret out of `authConfig` to keep the stored one. */\ntype UpdateAiToolParams = Partial<CreateAiToolParams>;\ninterface AiToolTestResult {\n success: boolean;\n /** What the AI would see, when the call succeeded. */\n result?: string;\n error?: string;\n statusCode?: number;\n durationMs: number;\n}\ninterface McpServerTool {\n name: string;\n description: string;\n /** The tool's input as JSON Schema. */\n inputSchema: Record<string, unknown>;\n}\ninterface McpServer {\n id: number;\n name: string;\n /** What the server is for and when the chatbot should use it. */\n description: string;\n url: string;\n authType: 'none' | 'bearer' | 'api_key';\n /** Header names and the like; secret values come back empty. */\n authConfig: Record<string, string> | null;\n /** Whether a secret is stored. */\n hasSecret: boolean;\n /** `false` disconnects the whole server from the chatbot. */\n enabled: boolean;\n /** What the server offered at the last sync. */\n tools: McpServerTool[];\n /** Names of the tools the chatbot may use. */\n enabledTools: string[];\n lastSyncedAt: string | null;\n /** Why the last sync failed, if it did. */\n lastError: string | null;\n /** Calls the chatbot made to this server, and how many failed. */\n callCount: number;\n failureCount: number;\n createdAt: string;\n}\ninterface CreateMcpServerParams {\n name: string;\n /**\n * 10-300 characters: what this server is for and when the chatbot should use it, e.g.\n * \"Order and stock lookups for our shop. Use when a customer asks about an order or an item.\"\n * The chatbot reads it with each of the server's tools to choose the right one.\n */\n description: string;\n /** The server's Streamable HTTP endpoint, a public address. */\n url: string;\n authType?: 'none' | 'bearer' | 'api_key';\n /** bearer: `{ token }` | api_key: `{ header, value }`. Write-only. */\n authConfig?: Record<string, string>;\n}\ninterface UpdateMcpServerParams {\n name?: string;\n description?: string;\n enabled?: boolean;\n /** Tool names to switch on (the rest are switched off). Counts against your plan's AI tool limit. */\n enabledTools?: string[];\n authType?: 'none' | 'bearer' | 'api_key';\n /** Leave a secret out to keep the stored one. */\n authConfig?: Record<string, string>;\n}\n\ndeclare class HttpError extends Error {\n readonly status: number;\n readonly body?: unknown | undefined;\n constructor(status: number, message: string, body?: unknown | undefined);\n}\ninterface RequestOptions {\n method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';\n path: string;\n params?: Record<string, string | number | undefined>;\n body?: unknown;\n}\ndeclare class HttpClient {\n private baseUrl;\n private apiKey;\n private orgId?;\n private timeout;\n constructor(baseUrl: string, apiKey: string, orgId?: string, timeout?: number);\n request<T>(opts: RequestOptions): Promise<T>;\n get<T>(path: string, params?: RequestOptions['params']): Promise<T>;\n post<T>(path: string, body?: unknown): Promise<T>;\n patch<T>(path: string, body?: unknown): Promise<T>;\n delete<T>(path: string): Promise<T>;\n}\n\ndeclare class ConversationsResource {\n private readonly http;\n constructor(http: HttpClient);\n list(params?: ListConversationsParams): Promise<{\n data: Conversation[];\n meta: PaginationMeta;\n }>;\n get(id: number): Promise<Conversation>;\n create(params: CreateConversationParams): Promise<Conversation>;\n update(id: number, params: UpdateConversationParams): Promise<Conversation>;\n resolve(id: number): Promise<Conversation>;\n close(id: number): Promise<Conversation>;\n assign(id: number, agentId: number): Promise<Conversation>;\n /** The same customer's earlier conversations, newest first (default 5, max 20). */\n getHistory(id: number, limit?: number): Promise<{\n data: PreviousConversation[];\n }>;\n /**\n * Hand a conversation from the AI assistant to a human: stops the bot and\n * assigns `agentId` (a team-member id) if nobody holds it yet. Fails with\n * 400 if the AI is no longer handling it.\n */\n takeOver(id: number, params: TakeOverParams): Promise<{\n success: boolean;\n conversationId: number;\n }>;\n getMessages(id: number, params?: ListMessagesParams): Promise<{\n data: Message[];\n meta: {\n hasMore: boolean;\n nextCursor?: string;\n };\n }>;\n sendMessage(id: number, params: SendMessageParams): Promise<Message>;\n delete(id: number): Promise<void>;\n}\n\ndeclare class CustomersResource {\n private readonly http;\n constructor(http: HttpClient);\n list(params?: {\n search?: string;\n page?: number;\n limit?: number;\n }): Promise<{\n data: Customer[];\n meta: PaginationMeta;\n }>;\n get(id: number): Promise<Customer>;\n upsert(params: UpsertCustomerParams): Promise<Customer>;\n update(id: number, params: Partial<UpsertCustomerParams>): Promise<Customer>;\n getConversations(id: number): Promise<{\n data: Conversation[];\n }>;\n delete(id: number): Promise<void>;\n}\n\ndeclare class VisitorsResource {\n private readonly http;\n constructor(http: HttpClient);\n /** Visitors from the last few days (online and offline) with visit counts and block/allow status. */\n list(params?: ListVisitorsParams): Promise<{\n data: Visitor[];\n meta: PaginationMeta & {\n truncated: boolean;\n };\n }>;\n /** Visitors on the site right now. */\n getActive(): Promise<{\n data: Visitor[];\n }>;\n /** Online now, countries and returning visitors (24h), active IP blocks, top countries. */\n getOverview(): Promise<VisitorsOverview>;\n /**\n * CSV of the visitors matching the filters (max 5,000). Requires a plan with\n * data exports (Pro and above) \u2014 otherwise throws an HttpError with status 403.\n */\n export(params?: Omit<ListVisitorsParams, 'page' | 'limit'>): Promise<VisitorsExport>;\n getPages(sessionId: string): Promise<{\n data: {\n url: string;\n title: string;\n visitedAt: string;\n }[];\n }>;\n block(sessionId: string, params?: BlockVisitorParams): Promise<{\n success: boolean;\n }>;\n}\n\ndeclare class AgentsResource {\n private readonly http;\n constructor(http: HttpClient);\n list(): Promise<{\n data: Agent[];\n }>;\n getOnline(): Promise<{\n data: {\n agentId: number;\n name: string;\n status: string;\n }[];\n }>;\n invite(params: InviteAgentParams): Promise<{\n success: boolean;\n message: string;\n }>;\n}\n\ndeclare class KnowledgeBaseResource {\n private readonly http;\n constructor(http: HttpClient);\n list(params?: {\n search?: string;\n category?: string;\n page?: number;\n limit?: number;\n }): Promise<{\n data: KbArticle[];\n meta: PaginationMeta;\n }>;\n /** Totals and the categories in use. */\n getStats(): Promise<KbStats>;\n /** What to write or fix next: most viewed, unanswered customer questions, lowest rated, and stale articles. */\n getInsights(): Promise<KbInsights>;\n get(id: number): Promise<KbArticle>;\n search(query: string): Promise<{\n data: KbArticle[];\n }>;\n createArticle(params: CreateArticleParams): Promise<KbArticle>;\n updateArticle(id: number, params: Partial<CreateArticleParams>): Promise<KbArticle>;\n deleteArticle(id: number): Promise<void>;\n}\n\ndeclare class WebhooksResource {\n private readonly http;\n constructor(http: HttpClient);\n list(): Promise<{\n data: Webhook[];\n }>;\n get(id: number): Promise<Webhook>;\n create(params: CreateWebhookParams): Promise<Webhook>;\n update(id: number, params: Partial<CreateWebhookParams & {\n isActive: boolean;\n }>): Promise<Webhook>;\n delete(id: number): Promise<void>;\n /**\n * Verify an incoming webhook signature.\n * The signature header is `X-Kerabie-Signature`.\n */\n static verify(rawBody: string, signature: string, secret: string): boolean;\n /**\n * Parse a verified webhook payload with full TypeScript types.\n */\n static parse<T = any>(rawBody: string): WebhookPayload<T>;\n}\n\ndeclare class AnalyticsResource {\n private readonly http;\n constructor(http: HttpClient);\n /** Conversation, response-time, satisfaction and AI numbers for the last `days` days (default 30), with deltas vs the previous period. */\n getSummary(params?: AnalyticsParams): Promise<AnalyticsSummary>;\n getAgentStats(params?: AnalyticsParams): Promise<{\n agents: AgentStat[];\n }>;\n /**\n * The deeper numbers: first-response times, per-agent and per-channel results, the questions customers ask,\n * who they are, when they write in, and which pages start conversations.\n */\n getInsights(params?: AnalyticsParams): Promise<AnalyticsInsights>;\n /** Open tickets by priority, and the percentage still within their SLA. */\n getTicketSla(): Promise<TicketSla>;\n /** Which of the first-run steps (connect a channel, train the AI, create an automation) are done. */\n getSetupStatus(): Promise<SetupStatus>;\n}\n\ndeclare class StoreResource {\n private readonly http;\n constructor(http: HttpClient);\n list(params?: {\n page?: number;\n limit?: number;\n }): Promise<{\n data: StoreProduct[];\n meta: PaginationMeta;\n }>;\n get(id: number): Promise<StoreProduct>;\n}\n\ndeclare class TicketsResource {\n private readonly http;\n constructor(http: HttpClient);\n list(params?: {\n status?: string;\n priority?: string;\n page?: number;\n limit?: number;\n }): Promise<{\n data: Ticket[];\n meta: PaginationMeta;\n }>;\n /** Open, unassigned and SLA-breaching counts plus the 7-day average resolution time and its change. */\n getStats(): Promise<TicketStats>;\n /**\n * CSV of tickets matching the filters (max 5,000). Requires a plan with\n * data exports (Pro and above) \u2014 otherwise throws an HttpError with status 403.\n */\n export(params?: ExportTicketsParams): Promise<TicketExport>;\n get(id: number): Promise<Ticket & {\n messages: TicketMessage[];\n }>;\n create(params: CreateTicketParams): Promise<{\n ticketId: number;\n message: TicketMessage;\n }>;\n update(id: number, params: UpdateTicketParams): Promise<Ticket>;\n resolve(id: number): Promise<Ticket>;\n close(id: number): Promise<Ticket>;\n reply(id: number, message: string): Promise<TicketMessage>;\n}\n\ndeclare class ContactsResource {\n private readonly http;\n constructor(http: HttpClient);\n /** People with a name, email or phone number, last-contacted first. */\n list(params?: ListContactsParams): Promise<{\n contacts: Contact[];\n meta: PaginationMeta;\n }>;\n /** Totals for the overview: everyone, new this week, leads, VIPs. */\n getStats(): Promise<ContactStats>;\n get(id: number): Promise<Contact>;\n /** Adds a contact. Needs a name, email or phone. Throws an HttpError with status 409 if the email is already saved. */\n create(params: CreateContactParams): Promise<{\n contact: Contact;\n }>;\n update(id: number, params: UpdateContactParams): Promise<{\n success: boolean;\n }>;\n /**\n * Imports up to 1,000 contacts per call. Rows whose email is already saved are skipped;\n * invalid rows are reported with their number instead of failing the whole import.\n */\n import(rows: ImportContactRow[]): Promise<ImportContactsResult>;\n}\n\ndeclare class CampaignsResource {\n private readonly http;\n constructor(http: HttpClient);\n /** Proactive messages shown on your chat widget, newest first. */\n list(params?: ListCampaignsParams): Promise<{\n campaigns: Campaign[];\n meta: PaginationMeta;\n }>;\n /** Totals and funnel numbers, plus how many campaigns your plan lets you run at once. */\n getStats(): Promise<CampaignStats>;\n get(id: number): Promise<Campaign>;\n /**\n * Saves a draft, or launches it with `isActive: true`.\n * Throws an HttpError with status 403 when launching would exceed your plan's live-campaign limit.\n */\n create(params: CreateCampaignParams): Promise<{\n campaign: Campaign;\n }>;\n /** Edits fields and/or turns it on or off with `isActive`. Send only what you want to change. */\n update(id: number, params: UpdateCampaignParams): Promise<{\n success: boolean;\n }>;\n /** A copy saved as a draft, with fresh results. */\n duplicate(id: number): Promise<{\n campaign: Campaign;\n }>;\n delete(id: number): Promise<{\n success: boolean;\n }>;\n}\n\ndeclare class AiToolsResource {\n private readonly http;\n constructor(http: HttpClient);\n /** Your AI agent tools (custom API actions the chatbot can call), with 30-day usage and your plan's limit. */\n list(): Promise<{\n tools: AiTool[];\n stats: AiToolStats;\n }>;\n /** One tool. Secrets are never returned. */\n get(id: number): Promise<AiToolDetail>;\n /**\n * Adds a tool. Throws an HttpError with status 403 when your plan has no room for another,\n * 409 when the function name is taken, and 400 for a private or local endpoint.\n */\n create(params: CreateAiToolParams): Promise<{\n tool: {\n id: number;\n };\n }>;\n /** Edits fields and/or switches the tool on or off with `enabled`. Send only what you want to change. */\n update(id: number, params: UpdateAiToolParams): Promise<{\n tool: {\n id: number;\n };\n }>;\n /** A copy that starts switched off. Counts against your plan's tool limit. */\n duplicate(id: number): Promise<{\n tool: {\n id: number;\n };\n }>;\n delete(id: number): Promise<{\n success: boolean;\n }>;\n /** Calls the tool's real endpoint once with `args`, the way the AI would. Not counted as usage. Limited to 40 a hour. */\n test(id: number, args?: Record<string, unknown>): Promise<AiToolTestResult>;\n}\n\n/** Remote MCP servers whose tools your AI chatbot can call, next to your own AI tools. */\ndeclare class McpServersResource {\n private readonly http;\n constructor(http: HttpClient);\n /** Connected servers, with the tools each offers and which are switched on. */\n list(): Promise<{\n servers: McpServer[];\n limit: number;\n }>;\n /**\n * Connects a server and reads its tools. No tool is switched on yet: use `update` with `enabledTools`.\n * Throws an HttpError with status 400 when the server can't be reached, 403 when your plan has no AI tools,\n * and 409 when it is already connected.\n */\n create(params: CreateMcpServerParams): Promise<{\n server: McpServer;\n }>;\n /** Reads the server's tools again. Switched-on tools the server no longer offers are dropped. */\n sync(id: number): Promise<{\n server: McpServer;\n }>;\n /** Renames, switches the server on or off, changes which tools the chatbot may use, or updates credentials. */\n update(id: number, params: UpdateMcpServerParams): Promise<{\n server: McpServer;\n }>;\n delete(id: number): Promise<{\n success: boolean;\n }>;\n /** Runs one of the server's tools once with `args`, the way the chatbot would. Limited to 40 a hour. */\n test(id: number, tool: string, args?: Record<string, unknown>): Promise<AiToolTestResult>;\n}\n\ndeclare class KerAbieClient {\n readonly conversations: ConversationsResource;\n readonly customers: CustomersResource;\n readonly visitors: VisitorsResource;\n readonly agents: AgentsResource;\n readonly kb: KnowledgeBaseResource;\n readonly webhooks: WebhooksResource;\n readonly analytics: AnalyticsResource;\n readonly store: StoreResource;\n readonly tickets: TicketsResource;\n readonly contacts: ContactsResource;\n readonly campaigns: CampaignsResource;\n readonly aiTools: AiToolsResource;\n readonly mcpServers: McpServersResource;\n private readonly http;\n constructor(config: KerAbieClientConfig);\n}\n\nexport { type Agent, type AgentInsight, type AgentStat, type AiTool, type AiToolAuthType, type AiToolDetail, type AiToolMethod, type AiToolParameter, type AiToolStats, type AiToolTestResult, type AiToolUsage, type AnalyticsInsights, type AnalyticsParams, type AnalyticsSummary, type Attachment, type BlockVisitorParams, type Campaign, type CampaignAction, type CampaignAudience, type CampaignFrequency, type CampaignStats, type CampaignStatus, type CampaignTrigger, type Contact, type ContactStats, type ContactType, type Conversation, type ConversationChannel, type ConversationCounts, type ConversationStatus, type CreateAiToolParams, type CreateArticleParams, type CreateCampaignParams, type CreateContactParams, type CreateConversationParams, type CreateMcpServerParams, type CreateTicketParams, type CreateWebhookParams, type Customer, type ExportTicketsParams, HttpError, type ImportContactRow, type ImportContactsResult, type InviteAgentParams, type KbArticle, type KbInsights, type KbStats, KerAbieClient, type KerAbieClientConfig, type ListCampaignsParams, type ListContactsParams, type ListConversationsParams, type ListMessagesParams, type ListVisitorsParams, type McpServer, type McpServerTool, type Message, type MessageType, type PaginationMeta, type PreviousConversation, type QuestionInsight, type SendMessageParams, type SenderType, type SetupStatus, type StoreProduct, StoreResource, type TakeOverParams, type Ticket, type TicketExport, type TicketMessage, type TicketPriority, type TicketSla, type TicketStats, type UpdateAiToolParams, type UpdateCampaignParams, type UpdateContactParams, type UpdateConversationParams, type UpdateMcpServerParams, type UpdateTicketParams, type UpsertCustomerParams, type Visitor, type VisitorStatus, type VisitorsExport, type VisitorsOverview, type Webhook, type WebhookEvent, type WebhookPayload, WebhooksResource };\n", "react-native": "import { ImageSourcePropType } from 'react-native';\n\nexport interface KerKbArticle {\n id: number;\n title: string;\n content: string;\n type: string;\n category: string;\n imageUrl?: string | null;\n views: number;\n updatedAt: string;\n}\n\nexport interface KerStoreProduct {\n id: number;\n name: string;\n description?: string | null;\n price: number;\n currency: string;\n imageUrl?: string | null;\n category?: string | null;\n stock?: number | null;\n}\n\nexport interface KerUser {\n id?: string;\n name?: string;\n email?: string;\n phone?: string;\n role?: string;\n customAttributes?: Record<string, string>;\n}\n\nexport interface KerTheme {\n primaryColor?: string;\n /** Gradient end color \u2014 only applied when colorType is 'gradient'. Mirrors the web dashboard's ChatWidget.secondaryColor. */\n secondaryColor?: string;\n colorType?: 'gradient' | 'singleColor';\n borderRadius?: 'none' | 'small' | 'medium' | 'large';\n /** Light/dark palette \u2014 mirrors the dashboard's ChatWidget.theme; 'auto' follows the device's color scheme. */\n mode?: 'light' | 'dark' | 'auto';\n /** Overrides the palette's background \u2014 leave unset to follow `mode`. */\n backgroundColor?: string;\n textColor?: string;\n bubbleColor?: string;\n bubbleIcon?: ImageSourcePropType;\n bubbleSize?: number;\n /** Size of the icon inside the floating button \u2014 mirrors the dashboard's ChatWidget.launcherIconSize (16\u201348). */\n bubbleIconSize?: number;\n fontFamily?: string;\n agentBubbleColor?: string;\n userBubbleColor?: string;\n /** Custom mascot/icon shown next to AI replies (ChatWidget.chatbotAvatarUrl) \u2014 MessageBubble falls back to a generic \u{1F916} when unset. */\n chatbotAvatarUrl?: string;\n}\n\nexport interface KerNotification {\n id: string;\n type: 'new_message' | 'conversation_assigned' | 'system';\n title: string;\n body: string;\n conversationId?: number;\n timestamp: string;\n}\n\nexport interface KerMessage {\n id: number | string;\n /** Set on messages from the server; absent on local optimistic bubbles. */\n conversationId?: number;\n body: string;\n createdAt: string;\n senderType: 'visitor' | 'agent' | 'bot';\n senderName?: string;\n senderAvatar?: string;\n /** Local-only, pre-send scaffolding for ctx.sendMessage(body, attachments) \u2014\n * the actual wire format (and what MessageBubble renders) is `type` +\n * `metadata`, matching the server's message shape 1:1 (see chat-cache.service.ts\n * CachedMessage). sendMessage() derives `type`/`metadata` from the first\n * entry here for its own optimistic bubble, same as the chat widget does. */\n attachments?: KerAttachment[];\n type?: 'text' | 'image' | 'file' | 'audio' | 'video' | 'system';\n /** `articles` is the KB citation(s) an AI reply matched (chatbot.service.ts buildContext). */\n metadata?: { url?: string; fileName?: string; mimeType?: string; articles?: Array<{ id: number; title: string }>; [key: string]: any };\n status?: 'pending' | 'sent' | 'delivered' | 'read' | 'failed';\n /** Set async, after the message arrives, via the 'chat:message:linkpreview' socket event. */\n linkPreview?: KerLinkPreview;\n /** True while an AI reply is still streaming in (chat:message:start..chat:message:chunk) \u2014 MessageBubble shows a blinking cursor instead of the sent/read ticks. */\n streaming?: boolean;\n}\n\nexport interface KerAttachment {\n id: string;\n /** For local optimistic display only (a local file:// URI or similar) \u2014\n * attachments are private objects; the real, fetchable URL only exists\n * server-side, resolved fresh per read from `key` (see chat.service.ts\n * resolveAttachmentUrls). Never assume this is fetchable once the\n * message has round-tripped through the server. */\n url: string;\n /** S3 object key \u2014 what actually gets sent to the server so it can\n * resolve a signed URL. Absent for a purely local/optimistic attachment\n * that hasn't finished uploading yet. */\n key?: string;\n type: 'image' | 'file' | 'audio' | 'video';\n name?: string;\n size?: number;\n}\n\nexport interface KerLinkPreview {\n url: string;\n title: string;\n description?: string;\n image?: string;\n favicon?: string;\n siteName?: string;\n}\n\nexport interface KerConversation {\n id: number;\n status: 'open' | 'resolved' | 'closed';\n assignedAgent?: { id: number; name: string; avatar?: string };\n lastMessage?: KerMessage;\n unreadCount: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport type KerTicketPriority = 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT';\nexport type KerTicketStatus = 'WAITING' | 'ACTIVE' | 'RESOLVED' | 'CLOSED';\n\nexport interface KerTicket {\n id: number;\n subject?: string | null;\n status: KerTicketStatus;\n priority: KerTicketPriority;\n slaDueAt?: string | null;\n createdAt: string;\n}\n\nexport interface KerTicketMessage {\n id: number | string;\n body: string;\n senderRole: 'agent' | 'customer';\n senderName?: string;\n createdAt: string;\n}\n\nexport type OpenMode = 'modal' | 'sheet' | 'push';\nexport type NavigationAdapter = 'expo-router' | 'react-navigation' | 'custom';\nexport type BubblePosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n\nexport interface KerNavigationRoute {\n /** The screen to navigate to */\n type: 'conversation' | 'inbox';\n /** Present when type === 'conversation' */\n conversationId?: number;\n}\n\nexport interface KerNotificationConfig {\n provider: 'expo';\n getToken?: () => Promise<string>;\n onTokenRegister?: (token: string) => void;\n onNotificationReceived?: (n: KerNotification) => void;\n}\n\n/** One question on the pre-chat form (dashboard: Messages > Pre-chat form). name/email/phone are the contact's own fields; other keys become custom attributes. */\nexport interface KerEnrichmentField {\n key: string;\n label: string;\n type: 'text' | 'email' | 'phone' | 'number' | 'textarea' | 'select';\n required: boolean;\n placeholder?: string;\n options?: string[];\n}\n\nexport interface KerChatSettings {\n /** Widget title shown in ChatScreen's own header \u2014 falls back to the org's dashboard-configured Widget Name (ChatWidget.name) when unset. */\n name?: string;\n welcomeMessage?: string;\n offlineMessage?: string;\n showBranding?: boolean;\n /** Show the \"agent is typing\" indicator \u2014 mirrors the dashboard's ChatWidget.typingIndicator. */\n typingIndicator?: boolean;\n enableFileAttachments?: boolean;\n enableVoiceMessages?: boolean;\n /** Whether the AI chatbot can reply when no agent is online \u2014 mirrors ChatWidget.chatbotEnabled. Gates the offline pre-chat capture form in ChatScreen. */\n chatbotEnabled?: boolean;\n /** 'single' = one continuous chat; the header menu (settings) only shows when showMenu is on. Mirrors ChatWidget.layout/showMenu. */\n layout?: 'full' | 'single';\n showMenu?: boolean;\n /** 'searchbar' = render <KerAbieSearchBar /> instead of the floating bubble. Mirrors ChatWidget.launcherType. */\n launcherType?: 'bubble' | 'searchbar';\n searchbarPlaceholder?: string;\n /** Ask visitors the pre-chat form's questions before their first message. Mirrors ChatWidget.customerEnrichmentEnabled/Form. */\n customerEnrichmentEnabled?: boolean;\n customerEnrichmentForm?: { fields: KerEnrichmentField[] } | null;\n}\n\nexport interface KerAbieProviderProps {\n widgetKey: string;\n appName?: string;\n appVersion?: string;\n appBuild?: string;\n openMode?: OpenMode;\n user?: KerUser;\n theme?: KerTheme;\n position?: BubblePosition;\n defaultOpen?: boolean;\n hideFloatingButton?: boolean;\n /**\n * Navigation adapter type.\n * - 'expo-router' \u2014 SDK uses expo-router internally (dynamic import)\n * - 'react-navigation' \u2014 SDK calls navigationRef.navigate()\n * - 'custom' \u2014 SDK calls onNavigate() callback\n * Only relevant when openMode='push'.\n */\n navigation?: NavigationAdapter;\n /** React Navigation navigationRef (required when navigation='react-navigation') */\n navigationRef?: React.RefObject<any>;\n /**\n * Called when the SDK needs to open a screen via your navigator.\n * Use this for 'custom' navigation or to override default expo-router/react-navigation behavior.\n * Only called when openMode='push'.\n */\n onNavigate?: (route: KerNavigationRoute) => void;\n onOpenChat?: () => void;\n onCloseChat?: () => void;\n /**\n * The app's current screen path (for example Expo Router's `usePathname()`).\n * Campaigns can be limited to certain screens (\"On pages\" in the dashboard); without this,\n * only campaigns for every screen are shown.\n */\n screen?: string;\n /**\n * A campaign button can open your Knowledge Base. Chat has no built-in screen for it, so the\n * app decides where it goes (e.g. `router.push('/help')`); without this the chat opens instead.\n */\n onOpenKnowledgeBase?: () => void;\n notifications?: KerNotificationConfig;\n chatSettings?: KerChatSettings;\n children: React.ReactNode;\n}\n\nexport interface KerAbieContextValue {\n // Chat state\n isOpen: boolean;\n open: () => void;\n close: () => void;\n toggle: () => void;\n unreadCount: number;\n conversations: KerConversation[];\n currentConversation: KerConversation | null;\n /** The identified user's name, used to fill {{name}} in the welcome message. */\n visitorName: string;\n messages: KerMessage[];\n sendMessage: (body: string, attachments?: KerAttachment[]) => Promise<void>;\n retryMessage: (msg: KerMessage) => Promise<void>;\n isTyping: boolean;\n /** True while the typing indicator is the AI chatbot (not a human agent) \u2014 ChatScreen shows the thinking orb for it. */\n isAiTyping: boolean;\n onlineAgents: number;\n isConnected: boolean;\n\n // User identity\n currentUser: KerUser | null;\n setUser: (user: KerUser) => void;\n clearUser: () => void;\n updateAttribute: (key: string, value: string) => void;\n\n // App identity\n appName: string;\n appVersion: string;\n appBuild: string;\n setAppVersion: (v: string) => void;\n\n // Notifications\n hasNotificationPermission: boolean;\n requestNotificationPermission: () => Promise<boolean>;\n registerNotificationToken: (token: string) => void;\n\n /** Text handed to ChatScreen when the search-bar launcher opens the chat (any staged attachment goes with it). */\n chatDraft: { text: string } | null;\n openWithDraft: (draft: { text: string }) => void;\n clearChatDraft: () => void;\n\n // Contextual chat\n openWithContext: (context: KerChatContext) => void;\n\n // Config\n config: Required<Pick<KerAbieProviderProps, 'openMode' | 'position' | 'hideFloatingButton'>> & {\n theme: KerTheme;\n widgetKey: string;\n /** From /public/widget/init \u2014 gates whether the host app should show a ticket entry point at all. */\n ticketsEnabled: boolean;\n launcherType: 'bubble' | 'searchbar';\n searchbarPlaceholder: string;\n };\n}\n\n/**\n * Contextual payload for opening chat with pre-loaded context.\n * Shown to the agent as context alongside the conversation.\n *\n * @example \u2013 product click\n * openWithContext({ type: 'product', id: 'sku_123', title: 'Blue Sneakers', data: { price: 4500 } })\n *\n * @example \u2013 order inquiry\n * openWithContext({ type: 'order', id: 'ORD-2024-001', title: 'Order #2024-001', data: { status: 'shipped' } })\n *\n * @example \u2013 fintech transaction\n * openWithContext({ type: 'transaction', id: 'TXN-987', title: '\u20A65,000 Transfer', data: { status: 'failed' } })\n *\n * @example \u2013 ride sharing\n * openWithContext({ type: 'ride', id: 'RIDE-456', title: 'Trip to Airport', data: { driver: 'John' } })\n */\nexport interface KerChatContext {\n type: 'order' | 'product' | 'transaction' | 'ride' | 'errand' | 'booking' | 'custom';\n id?: string; // external reference ID\n title?: string; // human-readable label shown to the agent\n data?: Record<string, any>; // arbitrary key-value payload\n openChat?: boolean; // auto-open the chat UI (default: true)\n}\n" } };
|
|
662
|
-
|
|
663
|
-
// src/knowledge.ts
|
|
664
|
-
var KNOWLEDGE = knowledge_default;
|
|
665
|
-
var DOCS = KNOWLEDGE.docs;
|
|
666
|
-
var ENDPOINTS = KNOWLEDGE.endpoints;
|
|
667
|
-
var TYPE_SOURCES = KNOWLEDGE.types;
|
|
668
|
-
var tokens = (s) => s.toLowerCase().split(/[^a-z0-9_]+/).filter((t) => t.length > 1);
|
|
669
|
-
function searchDocs(query, opts = {}, docs = DOCS) {
|
|
670
|
-
const words = [...new Set(tokens(query))];
|
|
671
|
-
if (!words.length) return [];
|
|
672
|
-
const hits = [];
|
|
673
|
-
for (const d of docs) {
|
|
674
|
-
if (opts.section && d.section !== opts.section) continue;
|
|
675
|
-
const title = `${d.title} ${d.id}`.toLowerCase();
|
|
676
|
-
const heads = d.headings.map((h) => h.text).join(" ").toLowerCase();
|
|
677
|
-
const body = d.body.toLowerCase();
|
|
678
|
-
let score = 0;
|
|
679
|
-
for (const w of words) {
|
|
680
|
-
if (title.includes(w)) score += 10;
|
|
681
|
-
if (heads.includes(w)) score += 4;
|
|
682
|
-
const n = body.split(w).length - 1;
|
|
683
|
-
if (n) score += Math.min(n, 5);
|
|
684
|
-
}
|
|
685
|
-
if (!score) continue;
|
|
686
|
-
hits.push({ id: d.id, title: d.title, section: d.section, url: d.url, snippet: snippetOf(d.body, words), score });
|
|
687
|
-
}
|
|
688
|
-
return hits.sort((a, b) => b.score - a.score).slice(0, Math.min(opts.limit ?? 5, 10));
|
|
689
|
-
}
|
|
690
|
-
function snippetOf(body, words) {
|
|
691
|
-
const lower = body.toLowerCase();
|
|
692
|
-
const at = words.map((w) => lower.indexOf(w)).filter((i) => i >= 0).sort((a, b) => a - b)[0] ?? 0;
|
|
693
|
-
const start = Math.max(0, at - 80);
|
|
694
|
-
return (start > 0 ? "\u2026" : "") + body.slice(start, start + 260).replace(/\s+/g, " ").trim() + (start + 260 < body.length ? "\u2026" : "");
|
|
695
|
-
}
|
|
696
|
-
function getDocText(doc, heading) {
|
|
697
|
-
if (!heading) return doc.body;
|
|
698
|
-
const lines = doc.body.split("\n");
|
|
699
|
-
const want = heading.trim().toLowerCase();
|
|
700
|
-
let fence = false;
|
|
701
|
-
let start = -1;
|
|
702
|
-
let level = 0;
|
|
703
|
-
for (let i = 0; i < lines.length; i++) {
|
|
704
|
-
if (lines[i].startsWith("```")) fence = !fence;
|
|
705
|
-
const m = !fence && lines[i].match(/^(#{2,3})\s+(.+)$/);
|
|
706
|
-
if (!m) continue;
|
|
707
|
-
if (start < 0 && m[2].trim().toLowerCase() === want) {
|
|
708
|
-
start = i;
|
|
709
|
-
level = m[1].length;
|
|
710
|
-
continue;
|
|
711
|
-
}
|
|
712
|
-
if (start >= 0 && m[1].length <= level) return lines.slice(start, i).join("\n").trim();
|
|
713
|
-
}
|
|
714
|
-
return start >= 0 ? lines.slice(start).join("\n").trim() : null;
|
|
715
|
-
}
|
|
716
|
-
function findDoc(id2, docs = DOCS) {
|
|
717
|
-
const clean = id2.trim().replace(/^https?:\/\/[^/]+\/docs\/?/, "").replace(/^\/+|\/+$/g, "").replace(/\.mdx?$/, "") || "index";
|
|
718
|
-
return docs.find((d) => d.id === clean);
|
|
719
|
-
}
|
|
720
|
-
function searchEndpoints(query, endpoints = ENDPOINTS) {
|
|
721
|
-
const words = tokens(query ?? "");
|
|
722
|
-
if (!words.length) return endpoints;
|
|
723
|
-
return endpoints.filter((e) => {
|
|
724
|
-
const hay = `${e.method} ${e.path} ${e.title} ${e.doc} ${e.sdk}`.toLowerCase();
|
|
725
|
-
return words.every((w) => hay.includes(w));
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
var DECL = /(?:export\s+)?(?:declare\s+)?(?:interface|type|class|enum|const|function)\s+([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
729
|
-
function listDeclarations(source) {
|
|
730
|
-
const names = /* @__PURE__ */ new Set();
|
|
731
|
-
for (const m of source.matchAll(DECL)) names.add(m[1]);
|
|
732
|
-
return [...names].sort();
|
|
733
|
-
}
|
|
734
|
-
function extractDeclaration(source, name) {
|
|
735
|
-
const re = new RegExp(`(?:export\\s+)?(?:declare\\s+)?(?:interface|type|class|enum)\\s+${name}\\b`);
|
|
736
|
-
const m = re.exec(source);
|
|
737
|
-
if (!m) return null;
|
|
738
|
-
let start = m.index;
|
|
739
|
-
const before = source.slice(0, start).trimEnd();
|
|
740
|
-
if (before.endsWith("*/")) {
|
|
741
|
-
const c = before.lastIndexOf("/**");
|
|
742
|
-
if (c >= 0) start = c;
|
|
743
|
-
}
|
|
744
|
-
let depth = 0;
|
|
745
|
-
let i = m.index;
|
|
746
|
-
let seenBrace = false;
|
|
747
|
-
for (; i < source.length; i++) {
|
|
748
|
-
const ch = source[i];
|
|
749
|
-
if (ch === "{") {
|
|
750
|
-
depth++;
|
|
751
|
-
seenBrace = true;
|
|
752
|
-
} else if (ch === "}") {
|
|
753
|
-
depth--;
|
|
754
|
-
if (seenBrace && depth === 0) {
|
|
755
|
-
i++;
|
|
756
|
-
if (source[i] === ";") i++;
|
|
757
|
-
break;
|
|
758
|
-
}
|
|
759
|
-
} else if (ch === ";" && depth === 0) {
|
|
760
|
-
i++;
|
|
761
|
-
break;
|
|
762
|
-
} else if (ch === "\n" && depth === 0 && seenBrace === false && /^\s*(?:export\s+)?(?:declare\s+)?(?:interface|type|class|enum|const|function)\s/.test(source.slice(i + 1, i + 60)) && i > m.index) break;
|
|
763
|
-
}
|
|
764
|
-
return source.slice(start, i).trim();
|
|
765
|
-
}
|
|
766
|
-
var SDK_GUIDES = {
|
|
767
|
-
typescript: ["sdks/typescript", "readme/typescript-sdk", "api-reference/authentication"],
|
|
768
|
-
"react-native": ["sdks/react-native", "readme/react-native-sdk"],
|
|
769
|
-
wordpress: ["sdks/wordpress"],
|
|
770
|
-
"web-widget": ["widget/overview", "widget/install-html", "widget/javascript-api", "widget/customization", "readme/chat-widget"],
|
|
771
|
-
"rest-api": ["api-reference/overview", "api-reference/authentication"],
|
|
772
|
-
mcp: ["sdks/mcp", "ai/mcp-servers"]
|
|
773
|
-
};
|
|
774
|
-
|
|
775
|
-
// src/dev-tools.ts
|
|
776
|
-
import { z as z2 } from "zod";
|
|
777
|
-
var MAX_DOC_CHARS = 4e4;
|
|
778
|
-
var cut = (text) => text.length > MAX_DOC_CHARS ? `${text.slice(0, MAX_DOC_CHARS)}
|
|
779
|
-
|
|
780
|
-
\u2026 (cut off. Ask for one section with the heading option.)` : text;
|
|
781
|
-
var DEV_TOOLS = [
|
|
782
|
-
{
|
|
783
|
-
name: "search_docs",
|
|
784
|
-
title: "Search the Kerabie docs",
|
|
785
|
-
local: true,
|
|
786
|
-
description: "Search the Kerabie documentation, SDK readmes and API reference. Use this first when asked how to do something with Kerabie. Returns the best matching pages with a snippet; read one with get_doc.",
|
|
787
|
-
input: { query: z2.string().min(2).describe('What you are looking for, e.g. "react native push notifications"'), section: z2.string().optional().describe("Limit to one section, e.g. api-reference, sdks, widget, ai"), limit: z2.number().int().min(1).max(10).optional() },
|
|
788
|
-
run: async (_c, a) => {
|
|
789
|
-
const hits = searchDocs(a.query, { section: a.section, limit: a.limit });
|
|
790
|
-
return hits.length ? hits.map(({ score: _s, ...h }) => h) : { results: [], hint: "Nothing matched. Try fewer or different words, or list_docs." };
|
|
791
|
-
}
|
|
792
|
-
},
|
|
793
|
-
{
|
|
794
|
-
name: "get_doc",
|
|
795
|
-
title: "Read a docs page",
|
|
796
|
-
local: true,
|
|
797
|
-
description: 'The full text of one docs page (or one section of it). Ids come from search_docs and list_docs, e.g. "api-reference/campaigns", "sdks/react-native" or "readme/react-native-sdk".',
|
|
798
|
-
input: { id: z2.string().min(1).describe("Page id or docs URL"), heading: z2.string().optional().describe("Return only the section under this heading") },
|
|
799
|
-
run: async (_c, a) => {
|
|
800
|
-
const d = findDoc(a.id);
|
|
801
|
-
if (!d) throw new Error(`No docs page "${a.id}". Use search_docs or list_docs to find the id.`);
|
|
802
|
-
const text = getDocText(d, a.heading);
|
|
803
|
-
if (text == null) throw new Error(`"${d.id}" has no section "${a.heading}". Sections: ${d.headings.map((h) => h.text).join(" | ")}`);
|
|
804
|
-
return cut(`# ${d.title}
|
|
805
|
-
${d.url ? `Source: ${d.url}
|
|
806
|
-
` : ""}
|
|
807
|
-
${text}`);
|
|
808
|
-
}
|
|
809
|
-
},
|
|
810
|
-
{
|
|
811
|
-
name: "list_docs",
|
|
812
|
-
title: "List the docs pages",
|
|
813
|
-
local: true,
|
|
814
|
-
description: "Every docs page with its id and description, optionally for one section (api-reference, sdks, widget, ai, conversations, \u2026).",
|
|
815
|
-
input: { section: z2.string().optional() },
|
|
816
|
-
run: async (_c, a) => DOCS.filter((d) => !a.section || d.section === a.section).map((d) => ({ id: d.id, title: d.title, description: d.description }))
|
|
817
|
-
},
|
|
818
|
-
{
|
|
819
|
-
name: "list_api_endpoints",
|
|
820
|
-
title: "List API endpoints",
|
|
821
|
-
local: true,
|
|
822
|
-
description: 'The public REST API endpoints (method, path, what it does, the docs page, and the equivalent TypeScript SDK call). Filter with words, e.g. "campaigns" or "POST tickets". Auth is a Bearer API key; see get_integration_guide rest-api.',
|
|
823
|
-
input: { query: z2.string().optional() },
|
|
824
|
-
run: async (_c, a) => searchEndpoints(a.query)
|
|
825
|
-
},
|
|
826
|
-
{
|
|
827
|
-
name: "get_sdk_types",
|
|
828
|
-
title: "Get SDK type definitions",
|
|
829
|
-
local: true,
|
|
830
|
-
description: 'TypeScript declarations from the Kerabie SDKs. Without names, lists what is declared. With names (e.g. ["Campaign", "CreateCampaignParams"]), returns those declarations. Use before writing code against an SDK.',
|
|
831
|
-
input: { sdk: z2.enum(["typescript", "react-native"]), names: z2.array(z2.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/)).max(20).optional() },
|
|
832
|
-
run: async (_c, a) => {
|
|
833
|
-
const src = TYPE_SOURCES[a.sdk];
|
|
834
|
-
if (!src) throw new Error(`No type definitions bundled for ${a.sdk}.`);
|
|
835
|
-
if (!a.names?.length) return { sdk: a.sdk, declarations: listDeclarations(src) };
|
|
836
|
-
const found = [];
|
|
837
|
-
const missing = [];
|
|
838
|
-
for (const n of a.names) {
|
|
839
|
-
const d = extractDeclaration(src, n);
|
|
840
|
-
(d ? found : missing).push(d ?? n);
|
|
841
|
-
}
|
|
842
|
-
return cut(found.join("\n\n") + (missing.length ? `
|
|
843
|
-
|
|
844
|
-
// Not found: ${missing.join(", ")}. Call without names to list declarations.` : ""));
|
|
845
|
-
}
|
|
846
|
-
},
|
|
847
|
-
{
|
|
848
|
-
name: "get_integration_guide",
|
|
849
|
-
title: "Get an integration guide",
|
|
850
|
-
local: true,
|
|
851
|
-
description: "The pages to follow to integrate Kerabie on one platform: install, configure and use. platform is typescript (the @kerabie/sdk package), react-native (live chat in an app), wordpress, web-widget (the website chat widget), rest-api (the HTTP API) or mcp.",
|
|
852
|
-
input: { platform: z2.enum(["typescript", "react-native", "wordpress", "web-widget", "rest-api", "mcp"]) },
|
|
853
|
-
run: async (_c, a) => {
|
|
854
|
-
const ids = SDK_GUIDES[a.platform] ?? [];
|
|
855
|
-
const pages = ids.map((id2) => findDoc(id2)).filter((d) => !!d);
|
|
856
|
-
if (!pages.length) throw new Error(`No guide for ${a.platform}.`);
|
|
857
|
-
return cut(pages.map((d) => `# ${d.title}
|
|
858
|
-
${d.url ? `Source: ${d.url}
|
|
859
|
-
` : ""}
|
|
860
|
-
${d.body}`).join("\n\n---\n\n"));
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
];
|
|
864
|
-
|
|
865
|
-
// src/server.ts
|
|
866
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
867
|
-
import { HttpError } from "@kerabie/sdk";
|
|
868
|
-
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
869
|
-
var MAX_RESULT_CHARS = 6e4;
|
|
870
|
-
var ALL_TOOLS = [...DEV_TOOLS, ...TOOLS];
|
|
871
|
-
var toolsFor = (readOnly, hasClient = true) => ALL_TOOLS.filter((t) => (hasClient || t.local) && !(readOnly && t.write));
|
|
872
|
-
function formatResult(value) {
|
|
873
|
-
const text = typeof value === "string" ? value : JSON.stringify(value ?? { success: true }, null, 2);
|
|
874
|
-
return text.length > MAX_RESULT_CHARS ? `${text.slice(0, MAX_RESULT_CHARS)}
|
|
875
|
-
\u2026 (cut off: ${text.length.toLocaleString("en-US")} characters. Use page/limit or filters to narrow the result.)` : text;
|
|
876
|
-
}
|
|
877
|
-
function formatError(e) {
|
|
878
|
-
if (e instanceof HttpError) {
|
|
879
|
-
const hint = e.status === 401 ? " Check your API key (KERABIE_API_KEY, or the Authorization header)." : e.status === 403 ? " The API key or your plan does not allow this." : e.status === 429 ? " Slow down and try again later." : "";
|
|
880
|
-
return `Kerabie API error ${e.status}: ${e.message}.${hint}`;
|
|
881
|
-
}
|
|
882
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
883
|
-
}
|
|
884
|
-
function createServer(client, opts = {}) {
|
|
885
|
-
const server = new McpServer({ name: "kerabie", version: opts.version ?? "1.0.0" });
|
|
886
|
-
for (const t of toolsFor(opts.readOnly ?? false, !!client)) {
|
|
887
|
-
server.registerTool(
|
|
888
|
-
t.name,
|
|
889
|
-
{
|
|
890
|
-
title: t.title,
|
|
891
|
-
description: t.description,
|
|
892
|
-
inputSchema: t.input,
|
|
893
|
-
annotations: { readOnlyHint: !t.write, destructiveHint: !!t.destructive, openWorldHint: t.name === "test_ai_tool" }
|
|
894
|
-
},
|
|
895
|
-
async (args) => {
|
|
896
|
-
try {
|
|
897
|
-
return { content: [{ type: "text", text: formatResult(await t.run(client, args)) }] };
|
|
898
|
-
} catch (e) {
|
|
899
|
-
return { isError: true, content: [{ type: "text", text: formatError(e) }] };
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
registerDocs(server);
|
|
905
|
-
return server;
|
|
906
|
-
}
|
|
907
|
-
var GUIDE_PROMPTS = [
|
|
908
|
-
{
|
|
909
|
-
name: "integrate_react_native",
|
|
910
|
-
title: "Add Kerabie chat to a React Native app",
|
|
911
|
-
description: "Walks an assistant through adding live chat to a React Native or Expo app.",
|
|
912
|
-
platform: "react-native",
|
|
913
|
-
task: 'Add Kerabie live chat to this React Native app. Call get_integration_guide with platform "react-native", then get_sdk_types for the types you use. Install the package and its peer dependencies, wrap the app in the provider, add the chat entry point, and show me what you changed. Do not invent props: use only what the guide and types define.'
|
|
914
|
-
},
|
|
915
|
-
{
|
|
916
|
-
name: "integrate_typescript_sdk",
|
|
917
|
-
title: "Use @kerabie/sdk in this project",
|
|
918
|
-
description: "Walks an assistant through calling the Kerabie API from TypeScript.",
|
|
919
|
-
platform: "typescript",
|
|
920
|
-
task: 'Integrate the Kerabie TypeScript SDK in this project. Call get_integration_guide with platform "typescript", then list_api_endpoints and get_sdk_types for what you need. Keep the API key in an environment variable, never in code. Handle HttpError by status (401, 403 plan limit, 404, 429).'
|
|
921
|
-
},
|
|
922
|
-
{
|
|
923
|
-
name: "integrate_rest_api",
|
|
924
|
-
title: "Call the Kerabie REST API",
|
|
925
|
-
description: "Walks an assistant through using the HTTP API directly.",
|
|
926
|
-
platform: "rest-api",
|
|
927
|
-
task: 'Call the Kerabie REST API from this project without the SDK. Call get_integration_guide with platform "rest-api", then list_api_endpoints for the routes you need and get_doc for their request and response shapes. Send the API key as a Bearer token from an environment variable.'
|
|
928
|
-
},
|
|
929
|
-
{
|
|
930
|
-
name: "install_web_widget",
|
|
931
|
-
title: "Install the Kerabie chat widget on a website",
|
|
932
|
-
description: "Walks an assistant through adding the chat widget to a website.",
|
|
933
|
-
platform: "web-widget",
|
|
934
|
-
task: 'Install the Kerabie chat widget on this website. Call get_integration_guide with platform "web-widget" and follow the install page that matches this site\u2019s stack. Do not guess the widget key: ask me for it.'
|
|
935
|
-
}
|
|
936
|
-
];
|
|
937
|
-
function registerDocs(server) {
|
|
938
|
-
for (const p of GUIDE_PROMPTS) {
|
|
939
|
-
server.registerPrompt(p.name, { title: p.title, description: p.description }, () => ({ messages: [{ role: "user", content: { type: "text", text: p.task } }] }));
|
|
940
|
-
}
|
|
941
|
-
server.registerResource(
|
|
942
|
-
"kerabie-docs",
|
|
943
|
-
new ResourceTemplate("kerabie-docs://{+id}", { list: async () => ({ resources: DOCS.map((d) => ({ uri: `kerabie-docs://${d.id}`, name: d.title, description: d.description, mimeType: "text/markdown" })) }) }),
|
|
944
|
-
{ title: "Kerabie documentation", description: "Every docs page and SDK readme as Markdown.", mimeType: "text/markdown" },
|
|
945
|
-
async (uri, variables) => {
|
|
946
|
-
const d = findDoc(String(variables.id));
|
|
947
|
-
if (!d) throw new Error(`No docs page "${variables.id}"`);
|
|
948
|
-
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: `# ${d.title}
|
|
949
|
-
|
|
950
|
-
${d.body}` }] };
|
|
951
|
-
}
|
|
952
|
-
);
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
export {
|
|
956
|
-
TOOLS,
|
|
957
|
-
searchDocs,
|
|
958
|
-
getDocText,
|
|
959
|
-
findDoc,
|
|
960
|
-
searchEndpoints,
|
|
961
|
-
listDeclarations,
|
|
962
|
-
extractDeclaration,
|
|
963
|
-
DEV_TOOLS,
|
|
964
|
-
MAX_RESULT_CHARS,
|
|
965
|
-
ALL_TOOLS,
|
|
966
|
-
toolsFor,
|
|
967
|
-
formatResult,
|
|
968
|
-
formatError,
|
|
969
|
-
createServer
|
|
970
|
-
};
|