Sales Core AI
Developer docs

Plug the AI salesperson into your product

One line puts the chat on any website. A small signed HTTP protocol lets the agent search your live catalog, place orders and book slots in your own system — every call validated, limited and recorded.

How it fits together

Sales Core AI is an AI salesperson that talks to your customers, qualifies them, recommends real items with exact prices, takes contacts, books and orders. You integrate it in two places.

CustomerWebsite chat · WhatsApp · Telegram
⇄

Sales Core AI agent

  • validates the input against your schema
  • checks business rules and handoffs
  • records every call and its result
signed tool calls →← signed order events
Your systemshop, CRM, booking/ai-sales/*
  • Website chat — a script tag on your pages. Nothing to build: the agent, knowledge and catalog are set up in the dashboard.
  • Connectors — when the catalog, orders or bookings live in your own system, expose them as tools over HTTPS. The agent calls them in the middle of a conversation; you send order status changes back.

The model decides, the platform controls

The AI chooses what to say and which tool to call. The platform fills in who the customer is and which conversation it is, validates the input, blocks actions after a handoff to a person and records the whole exchange. Your system never receives identifiers made up by the model.

Quick start: chat on your website

The chat is a small loader that draws a launcher button and opens the conversation in a secure frame. It works with any site builder or framework.

  1. 1

    Create an agent

    Sign up, pick your business type and let the setup assistant read your website. Test it in the agent’s test chat.

  2. 2

    Style the chat

    In Agent → Website chat choose colors, light or dark theme, position, greeting, launcher label and the proactive invitation.

  3. 3

    Paste the line

    Copy the snippet from the same page and put it before </body> on every page where the chat should appear.

  4. 4

    Allow your domains

    List the hosts that may show the chat, e.g. shop.example.com or *.example.com. An empty list means any site.

HTML
<script src="https://app.salescoreai.com/widget.js"
        data-agent-id="YOUR_AGENT_ID" async></script>

Language

The chat speaks your site’s language: data-lang on the script, otherwise the page’s <html lang> — and it follows when your site changes it. The built-in texts (input hint, buttons, errors, “the chat has ended”…) come in that language, and the agent uses it for its first words until the customer writes; after that it always replies in the customer’s language. Built-in texts exist in English, Russian, Ukrainian, Greek, Spanish and German; other languages get English texts, while the agent still follows the language.

HTML
<!-- Fixed language -->
<script src="https://app.salescoreai.com/widget.js" data-agent-id="YOUR_AGENT_ID" data-lang="ru" async></script>

<!-- Or nothing at all: the chat follows <html lang="…"> of the page -->
<html lang="de">
Texts you wrote yourself in the chat settings (greeting, subtitle, input hint) are shown as written, in any language.
Settings apply without touching your site: the chat reads them on every page load (cached for 15 seconds).

JavaScript API

When the loader is ready it exposes window.SalesCoreAI and fires an ai-sales:ready event on window with the same object in event.detail.

JavaScript
// Open, close or toggle the chat from your own button
SalesCoreAI.open();
SalesCoreAI.close();
SalesCoreAI.toggle();

// A signed-in visitor: the agent will not ask for what it already knows
SalesCoreAI.identify({ name: user.name, email: user.email, phone: user.phone });
MethodWhat it does
open()Opens the chat. The conversation frame is created on the first open, so the chat costs your page almost nothing until then.
close()Closes the chat window. The conversation is kept.
toggle()Opens or closes.
setLanguage(code)The site switched language without a reload ('ru', 'en-GB'…). The chat’s texts change at once and the agent gets the new language. Not needed if you change <html lang> — the chat follows it.
identify({ name, email, phone })Passes what your site knows about a signed-in visitor. Any field may be omitted; each is cut to 200 characters. The agent uses them instead of asking again, and they arrive in context.customer of connector calls.
JavaScript
// Your language switcher changed the site's language without a reload
SalesCoreAI.setLanguage('es');

Counting results in your analytics

When the agent books, places an order or saves the visitor’s contact, the chat fires a salescoreai:conversion event on window with event.detail.kind — booking, order or contact. Only the kind is sent, no personal data, and only for what happens on this visit. Send it to your analytics or ad platform — with the visitor’s consent where the law requires it.

JavaScript
window.addEventListener('salescoreai:conversion', (event) => {
  // 'booking' | 'order' | 'contact'
  if (event.detail.kind === 'booking') {
    gtag('event', 'conversion', { send_to: 'AW-XXXXXXXXX/XXXXXXXXXXXX' });
  }
});

Calling it before the script has loaded

The script is loaded with async, so your code may run first. Wait for the event:

function withChat(run) {
  if (window.SalesCoreAI) run(window.SalesCoreAI);
  else window.addEventListener('ai-sales:ready', (event) => run(event.detail), { once: true });
}

document.querySelector('#talk-to-sales').addEventListener('click', () => {
  withChat((chat) => chat.open());
});
Pages embedded earlier may use window.AISales — it is the same object and keeps working.

How the chat behaves

TopicBehaviour
IsolationThe launcher lives in a Shadow DOM, so your CSS cannot break it. The conversation runs in an iframe on our origin, so your scripts cannot read it.
Returning visitorsThe visitor’s conversation continues after a reload or on another page of the same site.
MobileBelow 480 px the chat opens full screen. Esc closes it on desktop.
Unread badgeReplies that arrive while the chat is closed (for example from a colleague who took over) show a counter on the launcher.
When it hidesThe launcher does not appear if the agent is paused, the chat is switched off, the balance is below zero, or the page’s host is not in the allowed domains. The browser console says which.
ColleaguesWhen a person takes over, the visitor sees who joined; the colleague’s replies appear in the same chat.

Connectors

A connector gives the agent the tools of another system: search your catalog, place an order there, check its status, book a slot. Protocol ai-sales-connector/1 is two HTTPS routes on your side and one webhook on ours.

DirectionRequestPurpose
Platform → youGET {baseUrl}/ai-sales/manifestWhich tools you offer, their roles and input schemas.
Platform → youPOST {baseUrl}/ai-sales/tools/{name}The agent calls one tool during a conversation.
You → platformPOST https://api.salescoreai.com/api/webhooks/connectors/{connectorId}An order changed status (paid, shipped, cancelled…).
You → platformGET https://api.salescoreai.com/api/connectors/{connectorId}/accountOptional: balance and usage to show in your admin.

Every request in both directions is signed with a shared secret. A connector belongs to an organization; one organization may connect several systems.

A minimal server

No SDK is required — any language that can compute HMAC-SHA256 works. For Node.js we also share @ai-sales/connector-sdk and a reference shop connector with teams that integrate: ask us.

import express from 'express';
import { verify } from './verify.js';

const app = express();
const secret = process.env.SALES_CORE_SECRET;
// The signature covers the exact bytes: keep the raw body.
app.use(express.text({ type: 'application/json' }));

const manifest = { protocol: 'ai-sales-connector/1', name: 'My shop', tools: [/* … */] };
const tools = {
  async search_mats(input, context) {
    const items = await db.findSets(input);
    return { success: true, data: { items } };
  }
};

app.get('/ai-sales/manifest', (req, res) => {
  if (!verify(secret, req.headers, '')) return res.status(401).end();
  res.json(manifest);
});

app.post('/ai-sales/tools/:name', async (req, res) => {
  if (!verify(secret, req.headers, req.body)) return res.status(401).end();
  const { input, context } = JSON.parse(req.body);
  const tool = tools[req.params.name];
  if (!tool) return res.json({ success: false, error: { code: 'UNKNOWN_TOOL', message: 'No such tool' } });
  try {
    res.json(await tool(input ?? {}, context));
  } catch (error) {
    console.error(error); // never send internals back: the model reads the message
    res.json({ success: false, error: { code: 'INTERNAL_ERROR', message: 'Could not process this request. Try again or hand over to a colleague.' } });
  }
});

app.listen(8080);

Manifest

The platform reads the manifest when you press Refresh tools in the dashboard. The request is signed, with an empty body.

GET /ai-sales/manifest
{
  "protocol": "ai-sales-connector/1",
  "name": "MatFit shop",
  "description": "Made-to-fit car mats: catalog with stock, orders and their status.",
  "testMode": true,
  "tools": [
    {
      "name": "search_mats",
      "role": "catalog.search",
      "description": "Find mat sets that fit a car. Returns sets with exact price and stock.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "make": { "type": "string" },
          "model": { "type": "string" },
          "year": { "type": "integer", "minimum": 1980 }
        },
        "required": ["make", "model"]
      }
    },
    {
      "name": "create_order",
      "role": "order.create",
      "requiresContact": true,
      "description": "Place an order for chosen sets with a delivery address.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "items": { "type": "array", "items": { "type": "object" } },
          "deliveryAddress": { "type": "string" }
        },
        "required": ["items"]
      }
    },
    {
      "name": "get_order_status",
      "role": "order.status",
      "description": "Status of the customer's orders.",
      "inputSchema": { "type": "object", "properties": {} }
    }
  ]
}
FieldMeaning
protocolAlways ai-sales-connector/1.
name, descriptionShown to the business owner in the dashboard.
testModeYour handlers honour context.test. See Test mode.
tools[].name^[a-z][a-z0-9_]{0,40}$. The agent sees it as <connector>__<name>.
tools[].roleWhat the tool means to the sales funnel. See Roles.
tools[].descriptionRead by the model: say when to use the tool, what to pass and what comes back.
tools[].inputSchemaJSON Schema of the input object. The platform validates the model’s input against it before calling you.
tools[].requiresContactCallable only once the customer’s phone or email is known.

Write descriptions for the model

The description is the tool’s instruction. “Find mat sets that fit a car. Pass make, model and year; optionally material. Returns sets with exact price and stock.” works far better than “Search”.

Roles

Semantics come from the role, not the tool’s name. A role can replace a built-in tool and moves the customer along the funnel when the call succeeds.

RoleMeaningEffect in the platform
catalog.searchSearch itemsReplaces the built-in catalog search. Non-empty data.items counts as items presented to the customer.
catalog.getOne itemReplaces the built-in item lookup.
catalog.imagesPictures of an itemPictures of an item in the options the customer chose (e.g. rendered in their colours): data.images with https URLs appears under the agent’s reply. Items from search may also carry images; the agent shows them when asked.
order.createPlace an orderReplaces the built-in order request. Success moves the customer to “order requested” and the order appears in Orders with your id.
order.statusThe customer’s ordersRead-only.
order.updateChange the customer’s orderAn action (idempotency key, blocked after a handoff); does not change the stage.
order.cancelCancel the customer’s orderAn action (idempotency key, blocked after a handoff); does not change the stage.
booking.availabilityFree slotsReplaces the built-in calendar lookup.
booking.createBook a slotReplaces built-in booking. Success confirms the appointment in the funnel.
booking.cancelCancel a bookingReplaces built-in cancellation.
lead.createSend the lead to your CRMAn action; does not change the stage.
customAnything elseNo special meaning.

Actions

Roles ending in .create, .cancel or .update are actions: they get an idempotencyKey and are blocked once the conversation is handed to a person.

Tool calls

The model supplies only input. Everything in context is filled in by the platform, and the request is signed, so you can trust it.

Request
POST {baseUrl}/ai-sales/tools/search_mats
Content-Type: application/json
X-AI-Sales-Timestamp: 1790331159
X-AI-Sales-Signature: sha256=5d41402abc4b2a76b9719d911017c592…

{
  "input": { "make": "Kia", "model": "Rio", "year": 2019 },
  "context": {
    "organizationId": "org_…",
    "conversationId": "conv_…",
    "customerId": "cust_…",
    "previousCustomerIds": ["cust_…"],
    "leadId": "lead_…",
    "customer": { "name": "Anna", "email": null, "phone": "+30…" },
    "channel": "WEB_CHAT",
    "timezone": "Europe/Athens",
    "idempotencyKey": "conv_…:create_order:3f2a…",
    "test": false
  },
  "requestId": "req_…"
}
Context fieldMeaning
organizationIdThe business the connector belongs to.
conversationIdThe conversation the call is made from.
customerIdThe customer; stable across channels once records are merged.
previousCustomerIdsEarlier ids of the same person. Present only after a merge.
leadIdThe customer’s lead, when there is one.
customername, email, phone the agent has learned so far (null when unknown).
channelWEB_CHAT, WHATSAPP or TELEGRAM.
timezoneThe organization’s IANA timezone, e.g. Europe/Athens.
idempotencyKeyActions only. A repeat call with the same key must return the same result without repeating the action.
testtrue in the owner’s test runs. See Test mode.

One person, one customer

Someone may write in Telegram first and on the website later. When the email or phone matches, the platform merges the records: customerId becomes the older id, and the others arrive in previousCustomerIds. Look orders up by all of them, or an order placed in another channel will seem to vanish.

JavaScript
const ids = [context.customerId, ...(context.previousCustomerIds ?? [])];
const orders = await db.orders.findMany({ where: { salesCoreCustomerId: { in: ids } } });

Prefill checkout

Store context.customer with the order or cart and prefill your payment form behind checkoutUrl, so the customer does not type their name and email again.

Responses

Always answer HTTP 200 with success and either data or error. Any other status means an infrastructure failure.

{
  "success": true,
  "data": {
    "items": [
      { "id": "MF-RIO-17-EVA", "name": "Kia Rio 2017–2023 · EVA · Full interior",
        "price": 89, "currency": "EUR", "material": "EVA", "inStock": true }
    ]
  }
}

The model reads error.message: write it so it knows what to do next — offer an alternative, ask for a missing detail, or hand over to a colleague. Never put stack traces or internal details there.

RoleShape of data
catalog.search{ items: [{ id, name, price, currency, … }], closestItems?: [...] } — closestItems are fitting alternatives when nothing matches exactly. Other simple fields are shown to the agent as the item’s attributes.
order.create{ orderId, status, items: [{ id, name, price, currency, quantity }], total, currency, deliveryAddress?, checkoutUrl? }
order.status{ orders: [{ orderId, status, … }] }
order.create
{
  "success": true,
  "data": {
    "orderId": "A-1042",
    "status": "NEW",
    "items": [{ "id": "MF-RIO-17-EVA", "name": "Kia Rio · EVA", "price": 89, "currency": "EUR", "quantity": 1 }],
    "total": 89,
    "currency": "EUR",
    "deliveryAddress": "12 Ermou St, Athens",
    "checkoutUrl": "https://shop.example.com/pay/A-1042"
  }
}

Payment button

If order.create returns an http(s) checkoutUrl, the customer gets a “Go to payment · total” button under the message, in their language, and the raw link is removed from the text. In Telegram it is an inline button (public https only), in WhatsApp a link line after the text.

Limits

LimitValue
Timeout15 seconds
Response size256 KB
What the model reads≈ 8,000 characters of JSON per result. Longer results are shortened for the model — nested lists first, then long strings, items last. The call log keeps the full response.
ErrorsNetwork errors and non-200 answers reach the model as CONNECTOR_UNAVAILABLE, without details.

Keep data compact

The model rereads every tool result on each following step of the turn, so size multiplies. Return a short summary per item in search and leave full variant lists and long descriptions to catalog.get.

Signing requests

The platform gives you a secret when the connector is created; it is shown once. Every request in both directions carries two headers.

Headers
X-AI-Sales-Timestamp: 1790331159
X-AI-Sales-Signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)>
  • The timestamp is Unix time in seconds. Reject requests more than 5 minutes off your clock.
  • Sign the exact raw body as sent, before any JSON parsing. For GET the body is an empty string.
  • Compare signatures in constant time.
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(secret, headers, rawBody) {
  const timestamp = headers['x-ai-sales-timestamp'] ?? '';
  const signature = headers['x-ai-sales-signature'] ?? '';
  if (!/^\d+$/.test(timestamp) || !signature.startsWith('sha256=')) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // 5 minutes

  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest();
  const given = Buffer.from(signature.slice(7), 'hex');
  return given.length === expected.length && timingSafeEqual(given, expected);
}
The most common mistake is verifying a re-serialized body. Frameworks that parse JSON automatically change whitespace and key order — read the body as text for these routes.

Order events

Order status changes come to the platform only through this signed webhook. PAID and FULFILLED close the sale, and commission is counted from it.

Event
{
  "type": "order.updated",
  "orderId": "A-1042",
  "orderNumber": "FE-009774",
  "status": "PAID",
  "note": "Paid by card"
}
FieldMeaning
typeAlways order.updated.
orderIdThe id your order.create returned.
statusCONFIRMED, CANCELLED, PAID, SHIPPED or FULFILLED.
orderNumberOptional. The number the customer sees in emails or receipts, when it differs from orderId. Shown in the orders list and to the agent, so the customer can name it in the chat.
noteOptional short comment.
BODY='{"type":"order.updated","orderId":"A-1042","status":"PAID"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SALES_CORE_SECRET" | sed 's/^.* //')

curl -X POST "https://api.salescoreai.com/api/webhooks/connectors/$CONNECTOR_ID" \
  -H "Content-Type: application/json" \
  -H "X-AI-Sales-Timestamp: $TS" \
  -H "X-AI-Sales-Signature: sha256=$SIG" \
  -d "$BODY"
AnswerMeaning
200 { "applied": true }The order was found and updated.
200 { "applied": false }No order with this id came from your system. Check that you send the orderId from order.create.
400 VALIDATION_ERRORUnknown event type or status.
401 INVALID_SIGNATUREWrong secret, a changed body or a clock off by more than 5 minutes. The owner is alerted.

Every event, accepted or rejected, is shown in the connector’s exchange log in the dashboard. Limit: 120 events per minute.

Test mode

Business owners save test dialogues and replay them after every change. Your connector decides how much of it reaches your system.

ToolsWithout testModeWith "testMode": true
Reads: catalog.*, order.status, booking.availabilityCalled as usual with context.test: trueCalled as usual with context.test: true
Actions and customNot called. The platform answers the agent with a plausible success, marked “simulated” in the reportCalled with context.test: true; the idempotency key starts with test:

If you declare testMode, a test call must run every check (stock, address, contact) and answer exactly like a real one, but change nothing: no order, booking or lead, no stock written off, no email sent.

JavaScript
async function createOrder(input, context) {
  const problem = await validateOrder(input, context.customer); // stock, address, contact
  if (problem) return { success: false, error: problem };

  if (context.test) {
    // Same answer as a real order, but nothing is stored, charged or emailed
    return { success: true, data: previewOrder(input) };
  }
  const order = await placeOrderOnce(context.idempotencyKey, input, context);
  return { success: true, data: order };
}
Do not write “this is a test” in the answer — the agent may repeat it to the customer.

Account endpoint

Optional. Show the business its Sales Core AI balance and monthly usage inside your own admin, with links to the dashboard.

A signed GET with an empty body, using the same secret. Limit: 60 requests per minute.

Response
GET https://api.salescoreai.com/api/connectors/{connectorId}/account

{
  "organization": { "name": "MatFit" },
  "connector": { "name": "MatFit shop", "status": "ACTIVE" },
  "balance": { "balanceUsd": 42.17, "lowBalanceUsd": 5, "low": false },
  "usage": { "month": "2026-09", "usageUsd": 12.4, "commissionUsd": 3.1, "turns": 318 },
  "links": {
    "dashboard": "https://app.salescoreai.com/dashboard",
    "billing": "https://app.salescoreai.com/billing",
    "connector": "https://app.salescoreai.com/connections/{connectorId}"
  }
}

An unknown connector, a wrong or expired signature all return 401 { "error": { "code": "INVALID_SIGNATURE" } }.

Connect it in the dashboard

  1. 1

    Add the system

    Connected systems → Connect a system: a name and your base URL. Copy the secret into your project’s environment — it is shown once (you can rotate it later).

  2. 2

    Load the tools

    Open the system and press Refresh tools. The tools and their roles appear; read tools can be tried with Test.

  3. 3

    Talk to the agent

    Use the agent’s test chat. Every call is visible in the conversation’s run with its input, result and timing.

  4. 4

    Send an event

    Move a test order to PAID in your system and watch it land in Orders and in the exchange log.

Connector addresses must be public http(s): localhost and private networks are refused. During development expose your local server through a tunnel.

Launch checklist

  • Signature checked on the raw body, with the 5-minute window, on every route.
  • Always HTTP 200 with success; error messages tell the agent what to do next.
  • Actions are idempotent by idempotencyKey.
  • Orders are found by customerId and previousCustomerIds.
  • Search results are compact; details live in catalog.get.
  • Answers well under 15 seconds.
  • order.updated is sent for every status change, with orderNumber if customers see a different number.
  • If you declare testMode, test calls change nothing.
  • The chat snippet is on every page, and your domains are in the allowed list.

Integrating a shop, CRM or booking system?

We share the Node.js SDK and a reference connector, and help you design the tools.

Write to us