Как всё устроено
Sales Core AI — это AI-продавец: он общается с вашими клиентами, выясняет потребность, предлагает реальные товары с точными ценами, берёт контакты, записывает и оформляет заказы. Интеграция — в двух местах.
Агент Sales Core AI
- проверяет ввод по вашей схеме
- соблюдает бизнес-правила и передачу человеку
- записывает каждый вызов и его результат
- Чат на сайте — тег script на ваших страницах. Писать код не нужно: агент, знания и каталог настраиваются в кабинете.
- Коннекторы — если каталог, заказы или запись живут в вашей системе, откройте их агенту как инструменты по HTTPS. Агент вызывает их прямо по ходу разговора, а вы присылаете обратно изменения статусов заказов.
Решает модель, контролирует платформа
AI выбирает, что сказать и какой инструмент вызвать. Платформа сама подставляет, кто клиент и какой это разговор, проверяет ввод, блокирует действия после передачи разговора человеку и записывает весь обмен. Идентификаторы, придуманные моделью, до вашей системы не доходят.Быстрый старт: чат на сайте
Чат — это небольшой загрузчик: он рисует кнопку и открывает разговор в защищённом фрейме. Работает с любым конструктором сайтов и фреймворком.
- 1
Создайте агента
Зарегистрируйтесь, выберите вид бизнеса и дайте помощнику настройки прочитать ваш сайт. Проверьте агента в тестовом чате.
- 2
Оформите чат
В Агент → Чат на сайте выберите цвета, светлую или тёмную тему, сторону экрана, приветствие, подпись кнопки и приглашение.
- 3
Вставьте строку
Скопируйте код на той же странице и поставьте его перед
</body>на всех страницах, где нужен чат. - 4
Разрешите домены
Укажите хосты, на которых чат может появляться, например
shop.example.comили*.example.com. Пустой список — любой сайт.
<script src="https://app.salescoreai.com/widget.js"
data-agent-id="YOUR_AGENT_ID" async></script>Язык
Чат говорит на языке вашего сайта: data-lang у скрипта, иначе <html lang> страницы — и переключается, когда сайт его меняет. Встроенные тексты (подсказка в поле ввода, кнопки, ошибки, «чат завершён»…) приходят на этом языке, а агент начинает на нём разговор, пока клиент не написал; дальше он всегда отвечает на языке клиента. Встроенные тексты есть на английском, русском, украинском, греческом, испанском и немецком; для других языков тексты будут английскими, а агент всё равно учтёт язык.
<!-- 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">JavaScript API
Когда загрузчик готов, он создаёт window.SalesCoreAI и отправляет в window событие ai-sales:ready с тем же объектом в event.detail.
// 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 });| Метод | Что делает |
|---|---|
open() | Открывает чат. Фрейм разговора создаётся при первом открытии, так что до этого чат почти ничего не стоит вашей странице. |
close() | Закрывает окно чата. Разговор сохраняется. |
toggle() | Открывает или закрывает. |
setLanguage(code) | Сайт сменил язык без перезагрузки ('ru', 'en-GB'…). Тексты чата меняются сразу, агент получает новый язык. Не нужно, если вы меняете <html lang>, — чат следит за ним сам. |
identify({ name, email, phone }) | Передаёт то, что сайт знает о вошедшем посетителе. Любое поле можно опустить; каждое обрезается до 200 символов. Агент не будет спрашивать это снова, а данные придут в context.customer вызовов коннектора. |
// Your language switcher changed the site's language without a reload
SalesCoreAI.setLanguage('es');Результаты в вашей аналитике
Когда агент записывает клиента, оформляет заказ или сохраняет контакт посетителя, чат отправляет в window событие salescoreai:conversion с event.detail.kind — booking, order или contact. Передаётся только вид события, без персональных данных, и только то, что случилось в этот визит. Отправляйте его в свою аналитику или рекламную систему — с согласия посетителя, где этого требует закон.
window.addEventListener('salescoreai:conversion', (event) => {
// 'booking' | 'order' | 'contact'
if (event.detail.kind === 'booking') {
gtag('event', 'conversion', { send_to: 'AW-XXXXXXXXX/XXXXXXXXXXXX' });
}
});Вызов до загрузки скрипта
Скрипт подключается с async, поэтому ваш код может выполниться раньше. Дождитесь события:
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());
});window.AISales — это тот же объект, он продолжает работать.Как ведёт себя чат
| Тема | Поведение |
|---|---|
| Изоляция | Кнопка живёт в Shadow DOM, поэтому CSS сайта её не сломает. Разговор идёт в iframe на нашем домене, поэтому скрипты сайта его не прочитают. |
| Возвращение посетителя | Разговор продолжается после перезагрузки и на других страницах того же сайта. |
| Телефоны | На экранах уже 480 px чат открывается на весь экран. На компьютере его закрывает Esc. |
| Непрочитанные | Ответы, пришедшие при закрытом чате (например, от подключившегося коллеги), показываются счётчиком на кнопке. |
| Когда чат скрыт | Кнопка не появится, если агент на паузе, чат выключен, баланс ниже нуля или хоста страницы нет в разрешённых доменах. Причину пишет консоль браузера. |
| Коллеги | Когда разговор берёт человек, посетитель видит, кто подключился; ответы коллеги приходят в тот же чат. |
Коннекторы
Коннектор даёт агенту инструменты другой системы: искать в вашем каталоге, оформлять там заказ, проверять его статус, записывать на время. Протокол ai-sales-connector/1 — это два HTTPS-маршрута у вас и один webhook у нас.
| Направление | Запрос | Зачем |
|---|---|---|
| Платформа → вы | GET {baseUrl}/ai-sales/manifest | Какие инструменты вы даёте, их роли и схемы ввода. |
| Платформа → вы | POST {baseUrl}/ai-sales/tools/{name} | Агент вызывает инструмент во время разговора. |
| Вы → платформа | POST https://api.salescoreai.com/api/webhooks/connectors/{connectorId} | Заказ сменил статус (оплачен, отправлен, отменён…). |
| Вы → платформа | GET https://api.salescoreai.com/api/connectors/{connectorId}/account | По желанию: баланс и расход, чтобы показать их в вашей админке. |
Каждый запрос в обе стороны подписан общим секретом. Коннектор принадлежит организации; одна организация может подключить несколько систем.
Минимальный сервер
SDK не обязателен — подойдёт любой язык, где есть HMAC-SHA256. Для Node.js мы также передаём командам, которые интегрируются, @ai-sales/connector-sdk и эталонный коннектор магазина: напишите нам.
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);Манифест
Платформа читает манифест, когда в кабинете нажимают Обновить инструменты. Запрос подписан, тело пустое.
{
"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": {} }
}
]
}| Поле | Смысл |
|---|---|
protocol | Всегда ai-sales-connector/1. |
name, description | Видны владельцу бизнеса в кабинете. |
testMode | Ваши обработчики учитывают context.test. См. Тестовый режим. |
tools[].name | ^[a-z][a-z0-9_]{0,40}$. Агент видит его как <коннектор>__<name>. |
tools[].role | Что инструмент значит для воронки продаж. См. Роли. |
tools[].description | Это читает модель: напишите, когда вызывать инструмент, что передавать и что вернётся. |
tools[].inputSchema | JSON Schema объекта ввода. Платформа проверяет по ней ввод модели до вызова. |
tools[].requiresContact | Вызов возможен, только когда известен телефон или email клиента. |
Пишите описания для модели
Описание — это инструкция к инструменту. «Find mat sets that fit a car. Pass make, model and year; optionally material. Returns sets with exact price and stock.» работает намного лучше, чем «Search».Роли
Смысл инструмента задаёт роль, а не имя. Роль может заменить встроенный инструмент и продвигает клиента по воронке при успешном вызове.
| Роль | Смысл | Эффект в платформе |
|---|---|---|
catalog.search | Поиск товаров | Заменяет встроенный поиск по каталогу. Непустой data.items считается показом товаров клиенту. |
catalog.get | Один товар | Заменяет встроенную карточку товара. |
catalog.images | Картинки товара | Картинки товара в выбранных клиентом опциях (например, отрисованные в его цветах): data.images с https-адресами показывается под ответом агента. Товары из поиска тоже могут нести images — агент покажет их, когда клиент попросит. |
order.create | Оформить заказ | Заменяет встроенную заявку на заказ. Успех переводит клиента в «заказ оформлен», заказ появляется в разделе Заказы с вашим id. |
order.status | Заказы клиента | Только чтение. |
order.update | Изменить заказ клиента | Действие (ключ идемпотентности, блокируется после передачи человеку); стадию не меняет. |
order.cancel | Отменить заказ клиента | Действие (ключ идемпотентности, блокируется после передачи человеку); стадию не меняет. |
booking.availability | Свободное время | Заменяет встроенный календарь. |
booking.create | Записать | Заменяет встроенную запись. Успех подтверждает визит в воронке. |
booking.cancel | Отменить запись | Заменяет встроенную отмену. |
lead.create | Передать лид в вашу CRM | Действие; стадию не меняет. |
custom | Всё остальное | Без особой семантики. |
Действия
Роли на.create, .cancel и .update — это действия: они получают idempotencyKey и блокируются, как только разговор передан человеку.Вызов инструмента
Модель передаёт только input. Всё в context заполняет платформа, и запрос подписан — этим данным можно доверять.
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 | Смысл |
|---|---|
organizationId | Бизнес, которому принадлежит коннектор. |
conversationId | Разговор, из которого сделан вызов. |
customerId | Клиент; один и тот же во всех каналах после склейки записей. |
previousCustomerIds | Прежние id того же человека. Есть только после склейки. |
leadId | Лид клиента, если он есть. |
customer | name, email, phone, которые агент уже узнал (null, если нет). |
channel | WEB_CHAT, WHATSAPP или TELEGRAM. |
timezone | Часовой пояс организации в формате IANA, например Europe/Athens. |
idempotencyKey | Только для действий. Повторный вызов с тем же ключом должен вернуть тот же результат, не повторяя действие. |
test | true в тестовых прогонах владельца. См. Тестовый режим. |
Один человек — один клиент
Человек может сначала написать в Telegram, а потом на сайте. Когда совпадает email или телефон, платформа склеивает записи: customerId становится старшим id, а остальные приходят в previousCustomerIds. Ищите заказы по всем этим id, иначе заказ из другого канала «пропадёт».
const ids = [context.customerId, ...(context.previousCustomerIds ?? [])];
const orders = await db.orders.findMany({ where: { salesCoreCustomerId: { in: ids } } });Предзаполните оплату
Сохраните context.customer вместе с заказом или корзиной и подставьте в форму оплаты по checkoutUrl, чтобы клиенту не пришлось заново вводить имя и email.
Ответ
Всегда отвечайте HTTP 200 с success и data либо error. Любой другой код считается сбоем инфраструктуры.
{
"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 }
]
}
}Текст error.message читает модель: пишите так, чтобы было понятно, что делать дальше, — предложить замену, уточнить недостающее или передать разговор коллеге. Никаких стектрейсов и внутренних деталей.
| Роль | Форма data |
|---|---|
catalog.search | { items: [{ id, name, price, currency, … }], closestItems?: [...] } — closestItems: подходящие альтернативы, когда точного совпадения нет. Остальные простые поля агент видит как атрибуты товара. |
order.create | { orderId, status, items: [{ id, name, price, currency, quantity }], total, currency, deliveryAddress?, checkoutUrl? } |
order.status | { orders: [{ orderId, status, … }] } |
{
"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"
}
}Кнопка оплаты
Если order.create вернул http(s)-ссылку checkoutUrl, клиент видит под сообщением кнопку «Перейти к оплате · сумма» на своём языке, а сырая ссылка убирается из текста. В Telegram это inline-кнопка (только публичный https), в WhatsApp — строка со ссылкой после текста.
Лимиты
| Лимит | Значение |
|---|---|
| Тайм-аут | 15 секунд |
| Размер ответа | 256 КБ |
| Что читает модель | ≈ 8 000 символов JSON на результат. Длиннее — укорачивается для модели: сначала вложенные списки, потом длинные строки, товары — в последнюю очередь. В журнале вызова ответ хранится целиком. |
| Ошибки | Сетевые сбои и ответы не 200 модель видит как CONNECTOR_UNAVAILABLE, без подробностей. |
Держите data компактным
Модель перечитывает каждый результат на всех следующих шагах хода, так что размер умножается. В поиске отдавайте короткую сводку по товару, а полные списки вариантов и длинные описания — вcatalog.get.Подпись запросов
Секрет выдаёт платформа при создании коннектора; он показывается один раз. Каждый запрос в обе стороны несёт два заголовка.
X-AI-Sales-Timestamp: 1790331159
X-AI-Sales-Signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)>- Timestamp — Unix-время в секундах. Отклоняйте запросы, расходящиеся с вашими часами больше чем на 5 минут.
- Подписывается точное сырое тело, до разбора JSON. Для
GETтело — пустая строка. - Сравнивайте подписи за постоянное время.
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);
}События заказов
Изменения статуса заказа попадают в платформу только через этот подписанный webhook. PAID и FULFILLED закрывают продажу, от неё считается комиссия.
{
"type": "order.updated",
"orderId": "A-1042",
"orderNumber": "FE-009774",
"status": "PAID",
"note": "Paid by card"
}| Поле | Смысл |
|---|---|
type | Всегда order.updated. |
orderId | Id, который вернул ваш order.create. |
status | CONFIRMED, CANCELLED, PAID, SHIPPED или FULFILLED. |
orderNumber | Необязательно. Номер, который клиент видит в письмах и чеке, если он отличается от orderId. Показывается в списке заказов и агенту — клиент может назвать его в чате. |
note | Необязательный короткий комментарий. |
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"| Ответ | Смысл |
|---|---|
200 { "applied": true } | Заказ найден и обновлён. |
200 { "applied": false } | Заказа с таким id от вашей системы нет. Проверьте, что отправляете orderId из order.create. |
400 VALIDATION_ERROR | Неизвестный тип события или статус. |
401 INVALID_SIGNATURE | Неверный секрет, изменённое тело или часы расходятся больше чем на 5 минут. Владелец получает оповещение. |
Каждое событие, принятое или отклонённое, видно в журнале обмена коннектора в кабинете. Лимит — 120 событий в минуту.
Тестовый режим
Владелец бизнеса сохраняет тестовые диалоги и прогоняет их после каждого изменения. Ваш коннектор решает, что из этого дойдёт до вашей системы.
| Инструменты | Без testMode | С "testMode": true |
|---|---|---|
Чтение: catalog.*, order.status, booking.availability | Вызываются как обычно, с context.test: true | Вызываются как обычно, с context.test: true |
Действия и custom | Не вызываются. Платформа сама отвечает агенту правдоподобным успехом, в отчёте — пометка «simulated» | Вызываются с context.test: true; ключ идемпотентности начинается с test: |
Если вы объявили testMode, тестовый вызов должен пройти все проверки (наличие, адрес, контакт) и ответить точно как настоящий, но ничего не менять: не создавать заказ, запись или лид, не списывать остатки, не отправлять письма.
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 };
}Данные аккаунта
По желанию. Покажите бизнесу баланс Sales Core AI и расход за месяц прямо в своей админке, со ссылками в кабинет.
Подписанный GET с пустым телом, тем же секретом. Лимит — 60 запросов в минуту.
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}"
}
}Неизвестный коннектор, неверная или просроченная подпись — всегда 401 { "error": { "code": "INVALID_SIGNATURE" } }.
Подключение в кабинете
- 1
Добавьте систему
Подключённые системы → Подключить систему: название и базовый адрес. Скопируйте секрет в переменные окружения проекта — он показывается один раз (потом его можно перевыпустить).
- 2
Загрузите инструменты
Откройте систему и нажмите Обновить инструменты. Появятся инструменты и их роли; инструменты чтения можно проверить кнопкой Проверка.
- 3
Поговорите с агентом
В тестовом чате агента. Каждый вызов виден в прогоне разговора — с вводом, результатом и временем.
- 4
Отправьте событие
Переведите тестовый заказ в
PAIDу себя и убедитесь, что он появился в Заказах и в журнале обмена.
Чек-лист перед запуском
- Подпись проверяется по сырому телу, с окном 5 минут, на каждом маршруте.
- Всегда HTTP 200 с
success; тексты ошибок подсказывают агенту, что делать дальше. - Действия идемпотентны по
idempotencyKey. - Заказы ищутся по
customerIdиpreviousCustomerIds. - Результаты поиска компактны; подробности — в
catalog.get. - Ответ приходит заметно быстрее 15 секунд.
order.updatedотправляется при каждой смене статуса, сorderNumber, если клиент видит другой номер.- Если объявлен
testMode, тестовые вызовы ничего не меняют. - Код чата стоит на всех страницах, а ваши домены — в списке разрешённых.
Подключаете магазин, CRM или систему записи?
Передадим Node.js SDK и эталонный коннектор, поможем спроектировать инструменты.