@ktzir/salesengine-sdk
v0.2.0
Published
Official Node.js SDK for the SalesEngine Developer API
Downloads
87
Readme
@ktzir/salesengine-sdk
Node.js client for the SalesEngine Developer API.
You send the customer's message plus whatever your backend already knows about the product, plan, or booking. The API returns text you can render in chat, and sometimes a suggested_action your app turns into a button (checkout, book, navigate, etc.). No catalog upload step — context travels with each request.
Node 18+. ESM only.
Install
npm install @ktzir/salesengine-sdkFirst request
Register at the developer portal. You get one sk_test_ key at signup (sandbox). Call the API from your server; don't ship keys to the browser.
import SalesEngine from "@ktzir/salesengine-sdk";
const se = new SalesEngine(process.env.SALESENGINE_API_KEY!);
const result = await se.chat.create({
message: "Can I book a GP visit this week?",
customer_id: "user_123",
context: {
business: {
name: "City Medical",
domain_description: "Family clinic — GP visits and referrals.",
},
page: {
type: "service_detail",
url: "https://citymed.example.com/services/gp-visit",
},
entities: [
{
type: "service",
id: "service_gp",
name: "GP consultation",
attrs: {
price: 15000,
currency: "NGN",
slots_available: true,
url: "https://citymed.example.com/book/gp",
},
action: {
type: "book_slot",
label: "Book appointment",
payload: { entity_id: "service_gp", url: "https://citymed.example.com/book/gp" },
},
},
],
},
});
console.log(result.reply);
if (result.suggested_action) {
// render label; on click, branch on suggested_action.type + payload
}Put real URLs in entity.attrs.url. Without them the model can still answer questions, but buttons won't deep-link anywhere useful.
Retail is the same shape — type: "product", action.type: "open_checkout", etc. See context docs or se.templates.get("retail") for a full request body.
Client setup
const se = new SalesEngine(process.env.SALESENGINE_API_KEY!, {
baseURL: "https://salesengine.ktzir.com/api/v1", // default
timeoutMs: 30_000,
fetchImpl: customFetch, // tests, Cloudflare Workers, etc.
});| Option | Default | Notes |
|--------|---------|--------|
| baseURL | https://salesengine.ktzir.com/api/v1 | Must end with /v1 |
| timeoutMs | 60000 | Per request |
| fetchImpl | globalThis.fetch | Swap for mocks |
Keys: sk_test_* hits the sandbox org (free caps). sk_live_* needs an active subscription or valid trial — otherwise you get 402. You only get one test key per workspace (at signup). Additional keys created in the portal are sk_live_* only.
Chat
se.chat.create
await se.chat.create({
message: string, // required
customer_id: string, // required — your stable user id; drives conversation memory
context?: object, // business, entities, policies, page
profile?: object, // industry, locale, sales_mode
idempotencyKey?: string // same key + body within 24h returns cached response
});Fields you'll actually use in the response:
reply— show in the chat bubblesuggested_action—{ type, label, entity_id, payload }when the API wants a next stepintent,stage,lead_score— if you're routing to CRM or scoring leadsmetadata.context_warnings— missing URLs, thin context, etc. Advisory only; request still succeedsmetadata.escalation_needed— hand off to a human when set
se.chat.qualify
Same request body as create, but no customer-facing reply — just intent, stage, score, and suggested_action. Handy when you only need routing metadata.
se.chat.voice
Multipart: audio in, transcript + chat fields + audio_base64 out.
const voice = await se.chat.voice({
file: audioBlob,
filename: "question.m4a",
customer_id: "user_123",
context: { business: { name: "City Motors" }, entities: [/* listing */] },
profile: { industry: "automobile" },
});If STT isn't trustworthy, transcript is empty, transcript_trusted is false, and suggested_action.type is retry_voice (with open_text_input as an alternate). The API won't invent a sales answer from garbage audio.
Voice on live keys needs Growth or Business (sandbox has demo STT/TTS caps). Starter is text-only.
se.audio.transcribe / se.audio.speech
Use these when you want STT or TTS without the combined voice turn:
const { text, metadata } = await se.audio.transcribe({ file: audioBlob, customer_id: "user_123" });
const { audio, mime } = await se.audio.speech({ text: "Your order is ready.", format: "mp3" });Check metadata.do_not_use_for_chat on transcribe if you're piping STT into your own logic.
Everything else on the data plane
await se.whoami(); // key valid? plan? voice caps?
await se.listActionTypes(); // canonical action types
await se.deleteCustomerMemory("user_123"); // wipe conversation history for a customer
const { example_request } = await se.templates.get("clinic");
const { presets } = await se.templates.listPresets();
await se.followUps.schedule({
customer_id: "user_123",
schedule_in_hours: 48,
message: "Still interested?",
context: {},
});followUps.schedule records the reminder and fires a follow_up.scheduled webhook. You send the message at scheduled_at through your own channel (email, push, SMS, in-app). We don't deliver it for you.
Errors
Non-2xx responses throw SalesEngineError:
import SalesEngine, { SalesEngineError } from "@ktzir/salesengine-sdk";
try {
await se.chat.create({ message: "Hi", customer_id: "u1", context: {} });
} catch (err) {
if (err instanceof SalesEngineError) {
console.error(err.status, err.code, err.message, err.requestId);
}
}Common cases: 401 bad key, 402 live key without billing, 429 quota/rate limit (back off), 400 test_key_not_creatable if you try to mint another sandbox key via the portal API.
Webhooks
Events are signed with HMAC-SHA256 in X-SalesEngine-Signature. Verify against the raw body string — if Express parses JSON first, verification breaks.
import express from "express";
import { constructWebhookEvent, WEBHOOK_SIGNATURE_HEADER } from "@ktzir/salesengine-sdk";
app.post(
"/webhooks/salesengine",
express.raw({ type: "application/json" }),
(req, res) => {
const event = constructWebhookEvent(
req.body,
req.headers[WEBHOOK_SIGNATURE_HEADER.toLowerCase()] as string,
process.env.WEBHOOK_SECRET!,
);
switch (event.type) {
case "chat.completed":
case "lead.qualified":
case "escalation.created":
case "follow_up.scheduled":
case "voice.fallback":
// handle event.data
break;
}
res.sendStatus(200);
},
);verifyWebhookSignature(rawBody, header, secret) is there if you don't want the parsed event object.
Return 2xx quickly. Do heavy work async on your side.
Portal client (DeveloperPortal)
Console routes (list keys, logs, webhooks) take a JWT from POST /auth/login, not an API key:
import { DeveloperPortal } from "@ktzir/salesengine-sdk";
const portal = new DeveloperPortal(process.env.SALESENGINE_PORTAL_JWT!);
const keys = await portal.keys.list();
const { secret } = await portal.keys.create({ name: "prod-worker-1", mode: "live" });
const logs = await portal.logs.list({ limit: 50 });
const usage = await portal.usage.summary();
await portal.webhooks.create({
url: "https://api.example.com/hooks/salesengine",
events: ["chat.completed", "lead.qualified", "voice.fallback"],
});Useful for internal tooling and CI — not for customer-facing request paths.
