npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-sdk

First 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 bubble
  • suggested_action{ type, label, entity_id, payload } when the API wants a next step
  • intent, stage, lead_score — if you're routing to CRM or scoring leads
  • metadata.context_warnings — missing URLs, thin context, etc. Advisory only; request still succeeds
  • metadata.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.

Reference