TL;DR: Настройте /whapi/webhook как публичный controller Odoo с csrf=False, подпишитесь на события Whapi messages.post и сохраняйте каждый message.id до создания crm.lead. Отправляйте stage-алерты через POST https://gate.whapi.cloud/messages/text и Authorization: Bearer. Протестируйте inbound на Community Edition в бесплатном Sandbox, прежде чем переводить канал в production.
Разработчики Odoo Community Edition поднимают intake CRM-лидов через inbound webhook Whapi и outbound REST в собственном Python-модуле. Copy-paste результат: webhook на входе, idempotent crm.lead, stage-алерт на выходе.
Если outbound-сообщения WhatsApp уходят, а ответы клиента так и не попадают в CRM — почти всегда виноват inbound webhook routing, а не API-токен. На форуме Odoo интеграторы описывают тот же паттерн: "Sending messages works, but incoming messages are not appearing in Odoo." Нативный коннектор Meta для Odoo требует Enterprise-лицензию и 2–7 рабочих дней business verification, прежде чем можно протестировать реальный pipeline. Whapi подключается QR-сканом и за минуты шлёт JSON на ваш controller.
Почему Whapi REST открывает intake CRM-лидов в Community
Enterprise блокирует нативный WhatsApp; Whapi REST открывает CRM intake в Community без Meta verification.
Официальное WhatsApp-приложение Odoo — только Enterprise. Команды на Community Edition упираются в стену: нет встроенного коннектора, нет поддерживаемого пути автосоздания crm.lead из чата. Модули Apps Store закрывают пробел за $50–$300+ с лицензиями OPL-1 — без исходников, когда inbound-трафик встаёт.
Whapi.Cloud подключается через web-session sockets — тот же механизм, что у WhatsApp Web. QR в dashboard, Bearer token в буфер, webhook URL в настройках. В официальном WhatsApp Business API business verification обычно занимает 2–7 рабочих дней до production messaging; с Whapi большинство команд отправляет тестовое inbound-сообщение в тот же день, когда ставят модуль.
Агентства недвижимости, e-commerce и клиники на Odoo CRM используют этот паттерн ежедневно: запрос в WhatsApp — scored lead, смена stage — follow-up reminder. Здесь мы пропускаем HMAC-проверку Meta hub.challenge; этот путь — для Enterprise Meta-коннекторов, а JSON webhook Whapi уже покрывает Community Edition.
Сравнение подходов к интеграции
Выберите Python-путь, который можно развернуть на Community Edition и отладить, когда inbound встаёт. В Odoo WhatsApp CRM проектах сегодня доминируют четыре варианта.
| Подход | Community Edition | Время setup | Владение кодом | Заметки по inbound |
|---|---|---|---|---|
| Нативный WhatsApp Odoo Enterprise | Нет | Дни (Meta verification + WABA) | Закрытый core-модуль | На форуме: outbound OK, inbound молчит до reset WABA/webhook |
| Apps Store OPL-1 (напр. whatsapp_lead_hook) | Да (зависит от модуля) | Часы (Meta tokens + install) | Closed source (~228+ LOC black box) | Всё ещё Meta WABA; troubleshooting ограничен |
| Custom Meta Cloud API Python controller | Да | Дни–недели | Полное (вы держите HMAC + token refresh) | Production number failures; tokens ~60 дней |
| Whapi REST + Odoo http.Controller (этот гайд) | Да | ~2 мин QR + install модуля | Полный Python-модуль под вашим контролем | JSON webhook; без template gate на stage notifications |
Модуль OCA mail_gateway_whatsapp — AGPL и Community-friendly, но всё равно требует Meta WABA, ручную настройку webhook в Facebook dashboard и test credentials, которые ротируются каждые 24 часа. Он маршрутизирует Discuss mail; idempotent crm.lead pipeline из коробки не даёт. Один Odoo-модуль с public route лучше, чем n8n посередине, когда нужен один stack trace controller для duplicate-lead bugs.
Структура файлов модуля
Соберите addon whapi_crm_lead: __manifest__.py, controllers/whatsapp_webhook.py, models/crm_lead.py, security/ir.model.access.csv. Установите на Odoo 17/18 Community через -i whapi_crm_lead. Whapi Sandbox (5 active conversations/месяц, 150 messages/день, бесплатно навсегда) — для проверки inbound parsing, пока production-трафик не бьёт в тот же controller.
Маппинг webhook-событий Whapi на поля Odoo
Parse payload Whapi, match partner phone, upsert open crm.lead, post в chatter. Эта цепочка из четырёх шагов — inbound-половина pipeline.
Whapi шлёт JSON WebhookPayload на ваш URL по событию messages.post. Массив messages содержит Message objects из справочника формата incoming webhooks. Игнорируйте строки, где from_me — true: это outbound echoes, не customer leads.
| Поле webhook Whapi | Цель Odoo | Заметки |
|---|---|---|
messages[].id |
crm.lead.x_whapi_message_id (Char, indexed) |
Ключ идемпотентности; проверка перед create |
messages[].from или chat_id |
res.partner.phone / mobile |
Нормализация в E.164; запасной вариант — последние 10 цифр |
messages[].text.body |
crm.lead.description + mail.message body |
Только когда type — text |
messages[].from_name |
crm.lead.contact_name |
Pushname, если есть |
messages[].timestamp |
context crm.lead.create_date |
Unix epoch от провайдера |
Webhook канала — через PATCH https://gate.whapi.cloud/settings: запись в массиве webhooks с HTTPS URL, mode: POST и events с messages. Shared secret в headers — и проверка в controller до parse JSON.
Сборка webhook controller
HTTP 200 — в течение секунды, даже если обработка lead идёт дальше. Whapi повторяет failed callbacks с linear backoff, если endpoint не отвечает 200.
Public route — потому что у серверов Whapi нет Odoo session cookie. CSRF отключайте только на этой route. Shared header — до request.jsonrequest. Event-reaction model здесь уместен: Whapi POST-ит event, controller реагирует, медленный handler — retries.
# controllers/whatsapp_webhook.py
import logging
from odoo import http
from odoo.http import request
_logger = logging.getLogger(__name__)
class WhapiWebhookController(http.Controller):
@http.route("/whapi/webhook", type="json", auth="public", csrf=False, methods=["POST"])
def whapi_inbound(self, **kwargs):
# Without the shared-secret check, anyone who discovers this URL can POST fake leads.
expected = request.env["ir.config_parameter"].sudo().get_param("whapi.webhook_secret")
incoming = request.httprequest.headers.get("X-Whapi-Secret")
if not expected or incoming != expected:
return {"status": "forbidden"}
payload = request.jsonrequest or {}
messages = payload.get("messages") or []
Lead = request.env["crm.lead"].sudo()
for msg in messages:
if msg.get("from_me"):
continue
Lead.process_whapi_inbound(msg)
return {"status": "ok"}
Whapi — на https://your-odoo-domain.com/whapi/webhook. POST https://gate.whapi.cloud/settings/webhook_test с тем же URL — reachability до live customer traffic.
Whapi требует публичный HTTPS URL. Команды в частной LAN — reverse proxy с TLS termination, иначе webhook test не пройдёт.
Идемпотентное создание leads
Сохраняйте message_id до create — повторы webhook тихо размножают CRM leads. Применяйте шлюз идемпотентности на каждом inbound event.
Whapi может доставить один и тот же payload messages.post повторно, если сервер медленный или status не 200. У Odoo нет native upsert для external webhook IDs. Разработчик, описавший стек Odoo 19 + n8n на dev.to, сформулировал прямо: дубликаты leads появляются, когда record создаётся на каждый POST без проверки unique message identifier.
# models/crm_lead.py
from odoo import api, fields, models
class CrmLead(models.Model):
_inherit = "crm.lead"
x_whapi_message_id = fields.Char(index=True, copy=False)
x_whapi_chat_id = fields.Char(copy=False)
@api.model
def process_whapi_inbound(self, msg):
message_id = msg.get("id")
if not message_id:
return
existing = self.search([("x_whapi_message_id", "=", message_id)], limit=1)
if existing:
return existing # idempotency gate: retry ignored
phone = msg.get("from") or msg.get("chat_id") or ""
body = (msg.get("text") or {}).get("body") or ""
partner = self.env["res.partner"]._find_or_create_from_whapi(phone, msg.get("from_name"))
open_lead = self.search([
("partner_id", "=", partner.id),
("type", "=", "opportunity"),
("active", "=", True),
("stage_id.is_won", "=", False),
], limit=1)
if open_lead:
open_lead.message_post(body=f"WhatsApp: {body}")
return open_lead
return self.create({
"name": f"WhatsApp - {partner.name or phone}",
"partner_id": partner.id,
"phone": partner.phone or phone,
"description": body,
"x_whapi_message_id": message_id,
"x_whapi_chat_id": msg.get("chat_id"),
})
Последующие сообщения объединяйте в открытую opportunity, пока stage_id.is_won — false. Новый lead — только после won или lost предыдущей сделки.
Исходящие уведомления о stage через Whapi REST
Смена stage в CRM запускает Whapi REST message — цикл inbound-outbound замкнут. Hook на записи stage_id, не только create events.
Lead на «Site Visit Scheduled» — клиенту WhatsApp confirmation без manual copy-paste. Override write() на crm.lead, детект stage_id changes, вызов text endpoint Whapi. В официальном WhatsApp Business API outbound вне 24-hour customer service window — pre-approved templates; Whapi шлёт session-style text через web-session sockets без template submission — stage alerts от CRM state changes.
# models/crm_lead.py (continued)
import os
import requests
from odoo import models
WHAPI_BASE = "https://gate.whapi.cloud"
class CrmLead(models.Model):
_inherit = "crm.lead"
def write(self, vals):
prev_stage = {lead.id: lead.stage_id for lead in self}
res = super().write(vals)
if "stage_id" not in vals:
return res
token = self.env["ir.config_parameter"].sudo().get_param("whapi.api_token")
for lead in self:
if prev_stage.get(lead.id) == lead.stage_id:
continue
to = lead.x_whapi_chat_id or lead.phone
if not (to and token):
continue
text = f"Hi {lead.contact_name or 'there'}, your deal moved to: {lead.stage_id.name}."
# Missing Bearer token returns 401; log it, do not crash the CRM write().
resp = requests.post(
f"{WHAPI_BASE}/messages/text",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"to": to, "body": text},
timeout=10,
)
if resp.status_code >= 400:
lead.message_post(body=f"Whapi stage alert failed: HTTP {resp.status_code}")
return res
Проверено по endpoint send text message: POST /messages/text требует to (телефон или Chat ID) и body. API token храните в ir.config_parameter, не в git.
Flat subscription Whapi держит расходы на stage notifications предсказуемыми — в отличие от per-template BSP billing на official paths.
Диагностика inbound-сбоев
Односторонняя синхронизация WhatsApp — inbound webhooks не работают, а не outbound configuration. Каждую отладку начинайте с проверки: Whapi доставил messages.post на ваш URL.
Симптомы в темах форума Odoo повторяются между версиями: шаблоны уходят, ответы пропадают. Тот же односторонний паттерн на Whapi — webhook URL указывает на устаревший staging tunnel после DNS cutover. После Meta verifications один пользователь написал: "This would mean the so-publicised whatsapp funcionality in Odoo is absolutely useless." На Whapi эквивалентная ошибка обычно локальная: неверный URL, нет HTTPS, неверный secret, Odoo возвращает 403 на public route из-за multi-database filter.
-
Webhook test: запустите
POST /settings/webhook_testиз Whapi dashboard. Ожидайте HTTP 200 и запись в Odoo logs за 2 секунды. -
messagesвключён в webhook entry? Connection health pings сами по себе leads не создадут. -
CSRF / auth: только Whapi route —
auth="public", csrf=False. JSON routes с portal auth отдают Whapi HTML login pages. -
Дубликаты строк? Проверьте
x_whapi_message_idна обоих records. Пустые значения — idempotency gate не сработал.
Если после этих проверок поведение неожиданное — напишите в поддержку Whapi.Cloud через chat widget на whapi.cloud. Команда помогает решать production webhook issues.
Подводные камни multi-database на staging
Controllers auth='public' ломаются на multi-DB hosts, если addon не загружен в server_wide_modules и database filter не направляет Whapi traffic в нужную CRM database. Симптом: webhook test возвращает HTTP 200 на пустую wrong DB, а production CRM молчит. Задайте явный dbfilter на public hostname, который регистрируете в Whapi.
Подключите Whapi inbound webhooks и outbound REST на Community Edition этим four-file module — Odoo/Python developers смогут протестировать full CRM pipeline в тот же день, когда отсканируют QR code.









