Вебхуки
Вебхуки дозволяють отримувати HTTP-сповіщення в реальному часі, коли в Looma відбуваються події — нові ліди та повідомлення чату.
Як це працює
- Ви створюєте вебхук з URL та списком подій
- Коли подія відбувається, Looma надсилає POST-запит на ваш URL
- Ваш сервер відповідає HTTP 2xx для підтвердження отримання
- У разі невдачі Looma повторює спробу з експоненційним відкатом
Підтримувані події
| Подія | Опис |
|---|---|
lead.created | Створено новий лід з чату |
chat.started | Новий запит на чат |
chat.message.new | Нове повідомлення в чат-сесії |
Створення вебхука
curl -X POST https://api.looma.chat/v1/webhooks \
-H "Authorization: Bearer lm_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/looma",
"events": ["lead.created", "chat.message.new"]
}'Відповідь містить secret, який показується лише один раз. Збережіть його надійно — він потрібен для верифікації підписів вебхуків.
Підписка на подію вимагає, крім webhooks:manage, права на читання її даних: leads:read для lead.created, chats:read для chat.started і chat.message.new (інакше 403 scope_denied). site_ids або не передається / null (усі сайти), або непорожній список ID сайтів; порожній список відхиляється з 422. Ключ, обмежений сайтами, підписується лише в їхніх межах.
Формат корисного навантаження
POST /webhooks/looma HTTP/1.1
Content-Type: application/json
X-Looma-Event: lead.created
X-Looma-Delivery: 25
X-Looma-Timestamp: 1700000000
X-Looma-Signature: sha256=a1b2c3d4e5f6...{
"id": 451,
"lead_id": 451,
"created_at": "2026-01-15T10:00:00+02:00",
"status": "new",
"name": "Olena",
"phone": "067 123 45 67",
"email": "[email protected]",
"address": null,
"lead_text": "Customer interested in Nike Air Max",
"custom_fields": { "City": "Lviv" },
"products": [
{
"product_id": "1024",
"title": "Nike Air Max 90",
"price": 4200.0,
"currency": "UAH",
"url": "https://shop.ua/air-max-90",
"image_url": "https://shop.ua/air-max-90.jpg"
}
],
"products_text": "Nike Air Max 90 — 4200 UAH",
"site_id": 8,
"site_name": "My Shop",
"site_domain": "shop.ua",
"source": "telegram",
"channel_id": 12,
"channel_name": "@shop_bot",
"utm_source": null,
"utm_medium": null,
"utm_campaign": null,
"landing_page": null,
"referrer": null,
"lead_url": "https://app.looma.chat/leads?id=451",
"session_id": "s_9f2c41a8",
"visitor_id": "v_58e51ff36a",
"collected_fields": { "name": "Olena", "phone": "067 123 45 67", "custom_1729000000000": "Lviv" },
"product_ids": [1024, 2048]
}Тіло lead.created — один плаский запис: кожен ключ присутній завжди (null, [] або {}, коли значення немає), тож отримувач мапить поля, не перевіряючи їхню відсутність. Той самий обʼєкт повертає GET /v1/leads?format=flat.
lead.created — знімок у момент створення ліда, зазвичай на першому контакті відвідувача. Поля, які пізніше в тій самій розмові доповнюють цей лід (пошта, ще товари, текст), повторно не надсилаються; GET /v1/leads?format=flat віддає лід таким, яким він є зараз, тож пізніше читання може містити більше, ніж подія.
| Поле | Тип | Значення |
|---|---|---|
id, lead_id | integer | ID ліда (обидва ключі мають однакове значення) |
created_at | string | ISO 8601 зі зсувом часового поясу сервера |
status | string | Статус ліда |
name, phone, email, address | string або null | Контакти; phone — так, як його ввів відвідувач (код країни не додається) |
lead_text | string або null | Що хотів відвідувач |
custom_fields | object | Власні зібрані поля за назвою з дашборда |
products | array | До 20 товарів: product_id, title, price, currency, url, image_url |
products_text | string | Один рядок назва — ціна валюта на товар, порожній, якщо товарів немає |
site_id, site_name, site_domain | integer / string / null | Сайт |
source | string | widget, telegram, viber, facebook, instagram, … |
channel_id, channel_name | integer / string / null | Канал месенджера, якщо є |
utm_source, utm_medium, utm_campaign, landing_page, referrer | string або null | Дані візиту |
lead_url | string | Посилання на лід у дашборді Looma |
session_id, visitor_id | string | Чат-сесія й відвідувач |
collected_fields | object | Зібрані поля як збережено |
product_ids | array | ID товарів як збережено (числа лишаються числами) |
Зворотна сумісність
lead_id, site_id, session_id, visitor_id, channel_id, lead_text, collected_fields, product_ids і created_at зберігають назви й типи, які були до плаского формату, — наявні отримувачі працюють далі. Решту полів додано поверх. Якщо плаский запис для ліда зібрати не вдалося, Looma надсилає початкове коротке тіло.
Решта подій (chat.started, chat.message.new) мають власні навантаження.
Дедуплікація
X-Looma-Delivery — це ID доставки, і він змінюється при кожному повторі, тож лід за ним не впізнати. Дедуплікуйте lead.created за id ліда.
Верифікація підписів
Кожен запит вебхука підписаний HMAC-SHA256. Завжди перевіряйте підпис, щоб впевнитися, що запит надійшов від Looma.
Підпис обчислюється як:
HMAC-SHA256(timestamp + "." + payload, secret)Приклад PHP
$payload = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_LOOMA_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_LOOMA_SIGNATURE'] ?? '';
// Заголовок несе префікс `sha256=` — зніміть його перед порівнянням.
$signature = str_starts_with($signature, 'sha256=') ? substr($signature, 7) : $signature;
$expected = hash_hmac('sha256', $timestamp . '.' . $payload, $webhookSecret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
// Опціонально: відхиляти запити старше 5 хвилин
if (abs(time() - (int)$timestamp) > 300) {
http_response_code(401);
exit('Timestamp too old');
}
$data = json_decode($payload, true);
// Обробка події...Приклад JavaScript
const crypto = require('crypto');
// Підпис рахується від СИРОГО тіла. express.json() розбере його наново, а
// повторний JSON.stringify змінить порядок ключів і екранування Unicode —
// HMAC не зійдеться ніколи. Тому беремо сирі байти.
app.post('/webhooks/looma', express.raw({ type: 'application/json' }), (req, res) => {
const payload = req.body.toString('utf8');
const timestamp = req.headers['x-looma-timestamp'];
const signature = (req.headers['x-looma-signature'] || '').replace(/^sha256=/, '');
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(timestamp + '.' + payload)
.digest('hex');
const ok =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) {
return res.status(401).send('Invalid signature');
}
// Назва події — у заголовку, а не в тілі.
const event = req.headers['x-looma-event'];
const data = JSON.parse(payload);
console.log(`Received: ${event}`, data);
res.status(200).send('OK');
});Політика повторних спроб
Якщо ваш сервер не відповідає HTTP 2xx, Looma повторює з експоненційним відкатом:
| Спроба | Затримка |
|---|---|
| 1-а повторна | 30 секунд |
| 2-а повторна | 2 хвилини |
| 3-я повторна | 10 хвилин |
Після 3 невдалих спроб доставку помічено як невдалу. Після 10 послідовних невдач різних доставок вебхук автоматично деактивується.
Деактивований вебхук можна повторно увімкнути через API або дашборд.
Відповідь 410 Gone
Якщо ваш ендпоінт відповідає HTTP 410 Gone, Looma сприймає це як «відписано»: вебхук одразу видаляється, без повторів. Так Zapier повідомляє Looma, що Zap вимкнено чи видалено. Відповідайте 410 лише тоді, коли вебхук справді більше не потрібен; будь-який інший не-2xx статус іде за політикою повторів вище.
Тестування
Використовуйте тестовий ендпоінт для надсилання зразкового навантаження:
curl -X POST https://api.looma.chat/v1/webhooks/5/test \
-H "Authorization: Bearer lm_your_key"Це надсилає тестову подію на URL вебхука та повертає статус відповіді від вашого сервера.
Ліміти
- Максимум 20 вебхуків на акаунт
- Таймаут доставки: 10 секунд
- Максимальний розмір навантаження: ~64 КБ

