Sales Core AI
Документация для разработчиков

Подключите AI-продавца к своему продукту

Одна строка ставит чат на любой сайт. Небольшой подписанный HTTP-протокол позволяет агенту искать в вашем живом каталоге, оформлять заказы и записывать клиентов в вашей системе — каждый вызов проверяется, ограничивается и записывается.

Как всё устроено

Sales Core AI — это AI-продавец: он общается с вашими клиентами, выясняет потребность, предлагает реальные товары с точными ценами, берёт контакты, записывает и оформляет заказы. Интеграция — в двух местах.

КлиентЧат на сайте · WhatsApp · Telegram
⇄

Агент Sales Core AI

  • проверяет ввод по вашей схеме
  • соблюдает бизнес-правила и передачу человеку
  • записывает каждый вызов и его результат
подписанные вызовы →← подписанные события
Ваша системамагазин, CRM, запись/ai-sales/*
  • Чат на сайте — тег script на ваших страницах. Писать код не нужно: агент, знания и каталог настраиваются в кабинете.
  • Коннекторы — если каталог, заказы или запись живут в вашей системе, откройте их агенту как инструменты по HTTPS. Агент вызывает их прямо по ходу разговора, а вы присылаете обратно изменения статусов заказов.

Решает модель, контролирует платформа

AI выбирает, что сказать и какой инструмент вызвать. Платформа сама подставляет, кто клиент и какой это разговор, проверяет ввод, блокирует действия после передачи разговора человеку и записывает весь обмен. Идентификаторы, придуманные моделью, до вашей системы не доходят.

Быстрый старт: чат на сайте

Чат — это небольшой загрузчик: он рисует кнопку и открывает разговор в защищённом фрейме. Работает с любым конструктором сайтов и фреймворком.

  1. 1

    Создайте агента

    Зарегистрируйтесь, выберите вид бизнеса и дайте помощнику настройки прочитать ваш сайт. Проверьте агента в тестовом чате.

  2. 2

    Оформите чат

    В Агент → Чат на сайте выберите цвета, светлую или тёмную тему, сторону экрана, приветствие, подпись кнопки и приглашение.

  3. 3

    Вставьте строку

    Скопируйте код на той же странице и поставьте его перед </body> на всех страницах, где нужен чат.

  4. 4

    Разрешите домены

    Укажите хосты, на которых чат может появляться, например shop.example.com или *.example.com. Пустой список — любой сайт.

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

Язык

Чат говорит на языке вашего сайта: data-lang у скрипта, иначе <html lang> страницы — и переключается, когда сайт его меняет. Встроенные тексты (подсказка в поле ввода, кнопки, ошибки, «чат завершён»…) приходят на этом языке, а агент начинает на нём разговор, пока клиент не написал; дальше он всегда отвечает на языке клиента. Встроенные тексты есть на английском, русском, украинском, греческом, испанском и немецком; для других языков тексты будут английскими, а агент всё равно учтёт язык.

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">
Тексты, которые вы сами написали в настройках чата (приветствие, подзаголовок, подсказка), показываются как есть, на любом языке.
Настройки применяются без правок на сайте: чат читает их при каждой загрузке страницы (кеш — 15 секунд).

JavaScript API

Когда загрузчик готов, он создаёт window.SalesCoreAI и отправляет в window событие ai-sales:ready с тем же объектом в 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 });
МетодЧто делает
open()Открывает чат. Фрейм разговора создаётся при первом открытии, так что до этого чат почти ничего не стоит вашей странице.
close()Закрывает окно чата. Разговор сохраняется.
toggle()Открывает или закрывает.
setLanguage(code)Сайт сменил язык без перезагрузки ('ru', 'en-GB'…). Тексты чата меняются сразу, агент получает новый язык. Не нужно, если вы меняете <html lang>, — чат следит за ним сам.
identify({ name, email, phone })Передаёт то, что сайт знает о вошедшем посетителе. Любое поле можно опустить; каждое обрезается до 200 символов. Агент не будет спрашивать это снова, а данные придут в context.customer вызовов коннектора.
JavaScript
// Your language switcher changed the site's language without a reload
SalesCoreAI.setLanguage('es');

Результаты в вашей аналитике

Когда агент записывает клиента, оформляет заказ или сохраняет контакт посетителя, чат отправляет в window событие salescoreai:conversion с event.detail.kind — booking, order или contact. Передаётся только вид события, без персональных данных, и только то, что случилось в этот визит. Отправляйте его в свою аналитику или рекламную систему — с согласия посетителя, где этого требует закон.

JavaScript
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);

Манифест

Платформа читает манифест, когда в кабинете нажимают Обновить инструменты. Запрос подписан, тело пустое.

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": {} }
    }
  ]
}
ПолеСмысл
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[].inputSchemaJSON 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Лид клиента, если он есть.
customername, email, phone, которые агент уже узнал (null, если нет).
channelWEB_CHAT, WHATSAPP или TELEGRAM.
timezoneЧасовой пояс организации в формате IANA, например Europe/Athens.
idempotencyKeyТолько для действий. Повторный вызов с тем же ключом должен вернуть тот же результат, не повторяя действие.
testtrue в тестовых прогонах владельца. См. Тестовый режим.

Один человек — один клиент

Человек может сначала написать в Telegram, а потом на сайте. Когда совпадает email или телефон, платформа склеивает записи: customerId становится старшим id, а остальные приходят в previousCustomerIds. Ищите заказы по всем этим id, иначе заказ из другого канала «пропадёт».

JavaScript
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, … }] }
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"
  }
}

Кнопка оплаты

Если 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);
}
Самая частая ошибка — проверять заново сериализованное тело. Фреймворки, которые сами разбирают JSON, меняют пробелы и порядок ключей, — для этих маршрутов читайте тело как текст.

События заказов

Изменения статуса заказа попадают в платформу только через этот подписанный webhook. PAID и FULFILLED закрывают продажу, от неё считается комиссия.

Событие
{
  "type": "order.updated",
  "orderId": "A-1042",
  "orderNumber": "FE-009774",
  "status": "PAID",
  "note": "Paid by card"
}
ПолеСмысл
typeВсегда order.updated.
orderIdId, который вернул ваш order.create.
statusCONFIRMED, 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, тестовый вызов должен пройти все проверки (наличие, адрес, контакт) и ответить точно как настоящий, но ничего не менять: не создавать заказ, запись или лид, не списывать остатки, не отправлять письма.

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 };
}
Не пишите в ответе «это тест» — агент может повторить это клиенту.

Данные аккаунта

По желанию. Покажите бизнесу баланс 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. 1

    Добавьте систему

    Подключённые системы → Подключить систему: название и базовый адрес. Скопируйте секрет в переменные окружения проекта — он показывается один раз (потом его можно перевыпустить).

  2. 2

    Загрузите инструменты

    Откройте систему и нажмите Обновить инструменты. Появятся инструменты и их роли; инструменты чтения можно проверить кнопкой Проверка.

  3. 3

    Поговорите с агентом

    В тестовом чате агента. Каждый вызов виден в прогоне разговора — с вводом, результатом и временем.

  4. 4

    Отправьте событие

    Переведите тестовый заказ в PAID у себя и убедитесь, что он появился в Заказах и в журнале обмена.

Адрес коннектора должен быть публичным http(s): localhost и внутренние сети не принимаются. Для разработки откройте локальный сервер через туннель.

Чек-лист перед запуском

  • Подпись проверяется по сырому телу, с окном 5 минут, на каждом маршруте.
  • Всегда HTTP 200 с success; тексты ошибок подсказывают агенту, что делать дальше.
  • Действия идемпотентны по idempotencyKey.
  • Заказы ищутся по customerId и previousCustomerIds.
  • Результаты поиска компактны; подробности — в catalog.get.
  • Ответ приходит заметно быстрее 15 секунд.
  • order.updated отправляется при каждой смене статуса, с orderNumber, если клиент видит другой номер.
  • Если объявлен testMode, тестовые вызовы ничего не меняют.
  • Код чата стоит на всех страницах, а ваши домены — в списке разрешённых.

Подключаете магазин, CRM или систему записи?

Передадим Node.js SDK и эталонный коннектор, поможем спроектировать инструменты.

Написать нам