API · v1 · 2026

Документация API L-ka

Эта страница нужна разработчикам, которые встраивают L-ka в свою CRM, BI или любой другой бэкенд. Здесь — настройка webhook'ов, HMAC-верификация, API-токены уровня workspace и публичные REST-эндпоинты /api/v1/*.

Base URL

URLhttps://app.l-ka.ru

Все REST-эндпоинты ниже — относительные к этому хосту.

Авторизация

В L-ka три типа авторизации. Для интеграции с CRM используется workspace token.

ТипКак получитьКогда использовать
JWT
user token, 30 дн.
POST /api/login с email+паролем Сессии конкретного пользователя в кабинете
wst_…
workspace token
Кабинет → Компания → API-токены → Создать Интеграции с CRM, BI, скриптами
bot_…
bot token, бессрочный
POST /api/botsPOST /api/bots/:id/token Боты, шлющие сообщения юзерам мессенджера

Использование workspace token

HTTPGET /api/v1/widgets HTTP/1.1
Host: app.l-ka.ru
Authorization: Bearer wst_x9k3Qm…

Если scope'ов токена не хватает — сервер вернёт 403 с указанием каких именно прав не хватает.

Безопасность. Токен виден только один раз при создании. В БД хранится sha256-хэш — вы не сможете его восстановить. Если потеряли — удалите старый и создайте новый.

Webhooks

Webhook — это HTTP POST из L-ka на ваш URL при событии виджета. Используется для уведомления CRM о новых сообщениях, оценках, запросах звонка и сменах статуса переписки.

Как настроить

Через кабинет:

  1. Откройте кабинет → раздел «Сайты» → выберите виджет.
  2. Введите Webhook URL (HTTPS, отвечающий 2xx за <5с).
  3. Опционально включите HMAC-подпись и сохраните secret.
  4. Выберите интересующие события (или оставьте «все»).
  5. Нажмите «Отправить тестовый запрос» и проверьте логи CRM.

Или через API:

curlcurl -X PATCH https://app.l-ka.ru/api/widgets/<widget_id> \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://crm.example.com/api/elka/webhook",
    "webhook_secret": "rotate",
    "webhook_events": ["message.from_visitor", "rating.received"]
  }'

Значение "rotate" в webhook_secret = «сгенерировать новый секрет». Если хотите задать сами — передайте строку 16..200 символов.

События

СобытиеКогда
message.from_visitorПосетитель отправил сообщение в виджет
message.from_operatorОператор / CRM ответили посетителю
conversation.assignedПереписка назначена на конкретного агента
conversation.status_changedСтатус переписки изменился (open / waiting / closed)
call.requestedПосетитель оставил номер для звонка
rating.receivedПосетитель оценил работу оператора (1-5)
pingТестовый запрос из «Отправить тестовый webhook»

Payload и заголовки

Каждый webhook приходит таким образом:

HTTPPOST /api/elka/webhook HTTP/1.1
Host: crm.example.com
Content-Type: application/json
User-Agent: Elka-Webhook/1.0
X-Elka-Event: message.from_visitor
X-Elka-Delivery: 4d7e2a-9c… # уникальный id попытки
X-Elka-Timestamp: 1737806400000
X-Elka-Signature: sha256=a3f9… # только если задан webhook_secret

{
  "event": "message.from_visitor",
  "at": 1737806400000,
  "widget_id": "…uuid…",
  "site_id": "st_xxxxxxxxxxxxxxxx",
  "data": {
    "visitor": {
      "id": "…",
      "visitor_uuid": "vu_…",
      "name": "Иван",
      "email": "ivan@example.com",
      "current_url": "https://shop.example.com/coffee"
    },
    "message": {
      "id": "…",
      "from": "visitor",
      "content": "Привет! Нужна помощь с заказом.",
      "type": "text",
      "created_at": 1737806400000
    }
  }
}
Retry-политика. 3 попытки с экспонентой 1с/2с/3с. Timeout 5с на попытку. «Успех» = HTTP 2xx. Если все попытки провалились — событие не повторяется. Ставьте идемпотентность по X-Elka-Delivery.

HMAC-верификация

Если для виджета задан webhook_secret, в каждом запросе будет заголовок X-Elka-Signature: sha256=<hex>. Формула:

PSEUDOsignature = HMAC-SHA256(
  key  = webhook_secret,
  data = X-Elka-Timestamp + "." + raw_request_body
)
Node.js · Expressconst crypto = require('crypto');

app.post('/api/elka/webhook',
  express.raw({ type: 'application/json' }), // !! важно: raw body
  (req, res) => {
    const ts  = req.headers['x-elka-timestamp'];
    const sig = req.headers['x-elka-signature'] || '';
    const body = req.body; // Buffer (raw)

    // 1. Защита от replay: окно ±5 минут
    if (Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000)
      return res.sendStatus(408);

    // 2. Сравнение constant-time
    const expected = 'sha256=' + crypto
      .createHmac('sha256', process.env.ELKA_WEBHOOK_SECRET)
      .update(ts + '.' + body)
      .digest('hex');
    if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)))
      return res.sendStatus(401);

    // 3. Парсим и обрабатываем
    const evt = JSON.parse(body);
    handleEvent(evt);
    res.sendStatus(200);
  });
Python · FastAPIimport hmac, hashlib, time
from fastapi import Request, HTTPException

@app.post("/api/elka/webhook")
async def webhook(req: Request):
    body = await req.body()
    ts   = req.headers.get("x-elka-timestamp", "0")
    sig  = req.headers.get("x-elka-signature", "")

    # Защита от replay
    if abs(int(time.time() * 1000) - int(ts)) > 5 * 60 * 1000:
        raise HTTPException(408)

    expected = "sha256=" + hmac.new(
        SECRET.encode(), (ts + ".").encode() + body, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(sig, expected):
        raise HTTPException(401)
    # … обработка …
    return {"ok": True}
Go · net/httpimport (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
)

func verify(req *http.Request, body []byte) bool {
  ts  := req.Header.Get("X-Elka-Timestamp")
  sig := req.Header.Get("X-Elka-Signature")
  mac := hmac.New(sha256.New, []byte(secret))
  mac.Write([]byte(ts + "."))
  mac.Write(body)
  expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
  return hmac.Equal([]byte(sig), []byte(expected))
}

Тестовый запрос из кабинета

В настройках виджета есть кнопка «Отправить тестовый webhook». Она шлёт настоящий запрос с теми же заголовками, но событие = ping:

PAYLOAD{
  "event": "ping",
  "at": 1737806400000,
  "widget_id": "…",
  "site_id": "st_…",
  "data": { "test": true }
}

Удобно подключить как первую проверку доставки.

Workspace API tokens

Если у вас компания (workspace) в L-ka — создайте токен один раз и используйте его в CRM. Никаких паролей менеджеров на сервере хранить не нужно.

Создать токен

POST/api/workspaces/:id/tokens
Auth: JWT · role ≥ admin
curlcurl -X POST https://app.l-ka.ru/api/workspaces/<ws_id>/tokens \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CRM raketaproduction",
    "scopes": ["widgets:read","visitors:read","messages:write"]
  }'

# Ответ — токен виден ОДИН раз:
{
  "id": "…",
  "token": "wst_x9k3QmRf8s2v…",
  "token_prefix": "wst_x9k3Qm",
  "scopes": ["widgets:read","visitors:read","messages:write"],
  "_warning": "Сохраните токен — он показывается только один раз."
}

Доступные scope'ы

ScopeДаёт
widgets:readСписок виджетов workspace
visitors:readСписок посетителей и их сообщения
messages:writeОтвечать посетителям от лица оператора
leads:createПомечать переписки как лиды (для будущих эндпоинтов)

Список / отзыв

GET/api/workspaces/:id/tokens · DELETE/api/workspaces/:id/tokens/:tid
Auth: JWT · role ≥ admin

REST API v1

Все эндпоинты ниже используют Authorization: Bearer wst_….

GET/api/v1/widgets
Scopes: widgets:read

Список виджетов workspace.

curlcurl https://app.l-ka.ru/api/v1/widgets \
  -H "Authorization: Bearer wst_…"
GET/api/v1/visitors?limit=50&widget_id=…
Scopes: visitors:read

Список последних посетителей. Сортировка по last_seen DESC. Лимит до 200.

GET/api/v1/visitors/:id/messages
Scopes: visitors:read

История сообщений с посетителем в хронологическом порядке.

POST/api/v1/visitors/:id/reply
Scopes: messages:write

Отправить ответ посетителю как будто это менеджер из кабинета. Доставка в виджет, webhook message.from_operator, событие в WS-канале посетителя.

curlcurl -X POST https://app.l-ka.ru/api/v1/visitors/<visitor_id>/reply \
  -H "Authorization: Bearer wst_…" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Привет! Сейчас уточню по заказу.",
    "operator_name": "Иван (CRM)"
  }'

Установка виджета на сайт

Вставьте перед закрывающим </body> любой страницы:

HTML<script src="https://app.l-ka.ru/widget/loader.js"
        data-site="st_xxxxxxxxxxxxxxxx"
        async></script>

data-site = site_id вашего виджета (виден в кабинете → Сайты → Код установки).

Iframe-embed. Приложение L-ka можно встраивать в iframe — мы разрешили это для crm.raketaproduction.ru. Если ваш CRM на другом домене — напишите на hello@l-ka.ru, добавим в whitelist.

Кастомизация виджета

Цвет, приветствие, позиция (слева/справа) и список разрешённых доменов задаются в кабинете → Сайты → редактор виджета. Сервер проверяет заголовок Origin входящих запросов от виджета и отдаёт 403, если домен не в whitelist (пустой список = разрешено везде).

Менять параметры программно через JS пока нельзя — в дорожной карте window-API ElkaWidget.setColor() и т.п.

Visitor-API виджета (без iframe)

Эти эндпоинты вызывает loader из iframe виджета. Документируем для тех, кто хочет встроить чат как native-компонент в SPA — то есть отрисовать UI самостоятельно, без нашего iframe. Авторизация — по visitor_token, получаемому из POST /init.

GET/api/widget/:siteId/config
Auth: public · по data-site

Публичный конфиг виджета: цвет, приветствие, позиция, allowed_origins.

200 OK{
  "id": "…",
  "site_id": "st_…",
  "name": "Главный сайт",
  "color": "#004AAD",
  "greeting": "Здравствуйте! Чем могу помочь?",
  "position": "right",
  "allowed_origins": ["example.ru"]
}
POST/api/widget/:siteId/init
Auth: public

Инициализация сессии. Передайте visitor_uuid из localStorage (если уже есть) — иначе сервер сгенерит новый и вернёт. В ответ — visitor_token, его надо посылать в каждом следующем запросе.

Request{
  "visitor_uuid": "vu_existing_or_new",
  "origin": "https://example.ru/cart",
  "first_page": "/cart"
}
200 OK{
  "visitor_token": "vt_…",
  "visitor_id": "…",
  "messages": [ /* предыдущая переписка */ ]
}
POST/api/widget/:siteId/message
Header: X-Visitor-Token: vt_…

Отправить сообщение от посетителя.

Request{
  "content": "Здравствуйте, есть в наличии товар X?",
  "type": "text"  // или "image" / "file" после upload
}
200 OK{
  "id": "…",
  "visitor_id": "…",
  "from": "visitor",
  "content": "…",
  "type": "text",
  "read": false,
  "created_at": 1737806400000
}
POST/api/widget/:siteId/upload
Header: X-Visitor-Token: vt_… · Content-Type: multipart/form-data

Загрузить файл (поле file). В ответ — публичный URL. Дальше отправляйте как сообщение с type:"image" и file_url.

200 OK{ "url": "/uploads/abc123.jpg" }

Идентификация посетителя

Если сайт знает, кто посетитель (залогинен в личном кабинете), передайте имя и email — оператор увидит сразу с кем общается, а данные пробросятся в webhook'и и web-chat сессию CRM.

POST/api/widget/:siteId/identify
Header: X-Visitor-Token: vt_…
Request{
  "name": "Анна Иванова",
  "email": "anna@example.com",
  "phone": "+7 999 …"  // необязательно
}
200 OK{ "ok": true }

Если используете официальный loader-виджет — будущее window-API:

JS · draftwindow.ElkaWidget.identify({
  name: 'Анна Иванова',
  email: 'anna@example.com'
});
Window-API ElkaWidget.identify в активной разработке. Пока используйте прямой fetch с visitor_token из localStorage.

Realtime · Socket.IO

Виджет подключается к Socket.IO namespace /widget для получения ответов оператора в реальном времени без polling-а.

JSconst socket = io('https://app.l-ka.ru/widget', {
  auth: { visitor_token: 'vt_…' }
});

// Войти в комнату конкретного visitor
socket.emit('join', { visitor_id: '…' });

// Слушать новые сообщения от оператора
socket.on('new_message', (msg) => {
  // { id, visitor_id, from:'owner', content, type, file_url, created_at }
  console.log('Ответ оператора:', msg.content);
});

// Индикатор «оператор печатает»
socket.on('agent_typing', () => { /* показать "..." */ });

Для серверных интеграций Socket.IO не нужен — используйте webhooks (выше). WS — только для visitor-side UI.

JWT login (legacy)

Если вам нужно действовать от лица конкретного оператора (например, при разработке собственного клиента мессенджера), используйте JWT-логин. Для интеграций с CRM это не нужно — workspace tokens (выше) проще, безопаснее и не требуют хранить пароль.

POST/api/login
Auth: public
Request{ "login": "agent_username", "password": "…" }
200 OK{
  "token": "eyJhbGc…",    // JWT, 30 дней
  "user": { "id": "…", "username": "…", "display_name": "…" }
}

Дальше во всех запросах: Authorization: Bearer <token>. С этим токеном работают legacy-эндпоинты:

ЭндпоинтЧто
GET /api/workspaces/mineМои компании
GET /api/workspaces/:idПодробности компании (только member)
GET /api/workspaces/:id/widgetsВиджеты компании
POST /api/workspaces/:id/widgetsСоздать виджет (admin+)
POST /api/workspaces/:id/invite-linksСсылка-приглашение в команду
POST /api/workspaces/:id/tokensСоздать workspace API token (см. Workspace tokens)

Полный список — в исходниках server.js. Стабильное публичное REST API — это /api/v1/* выше; legacy маршруты могут меняться без депрекейшна.