Odoo · Integraciones · Webhooks
Un webhook que falla en silencio es peor que una integración que no existe: da la ilusión de que los datos están sincronizados cuando en realidad llevan días desincronizados. Recibir eventos externos en Odoo exige pensar en reintentos e idempotencia desde la primera línea, no como un añadido posterior.
El endpoint
Un controlador HTTP para recibir el evento, no para procesarlo
Un webhook entrante en Odoo es un controlador con auth='public' (o con validación de firma propia) que responde rápido y delega el trabajo pesado. Procesar el payload de forma síncrona dentro del propio request es arriesgado: si el proveedor externo tiene un timeout corto, cualquier lentitud en tu ORM hace que reintente el mismo evento y lo duplique.
# controllers/webhook.py
from odoo import http
from odoo.http import request
class WebhookController(http.Controller):
@http.route("/webhook/pedidos", type="json", auth="public", methods=["POST"], csrf=False)
def recibir_pedido(self, **payload):
if not self._firma_valida(request.httprequest):
return {"error": "firma inválida"}
request.env["mi_modulo.evento_webhook"].sudo().create({
"payload": str(payload),
"estado": "pendiente",
})
return {"ok": True}
Procesar después
Guarda el evento crudo y procésalo con un cron o queue_job
Separar "recibir" de "procesar" te da margen: si el procesado falla, el evento crudo sigue ahí para reintentarlo sin pedirle al proveedor externo que reenvíe nada. Un modelo simple con estado (pendiente / procesado / error) y un cron que lo recorra es suficiente para la mayoría de integraciones; para volumen alto, queue_job evita bloquear el worker principal.
# models/evento_webhook.py
class EventoWebhook(models.Model):
_name = "mi_modulo.evento_webhook"
payload = fields.Text()
estado = fields.Selection([
("pendiente", "Pendiente"),
("procesado", "Procesado"),
("error", "Error"),
], default="pendiente")
intentos = fields.Integer(default=0)
def _procesar(self):
for evento in self:
try:
evento._aplicar_al_pedido()
evento.estado = "procesado"
except Exception as exc:
evento.intentos += 1
evento.estado = "error" if evento.intentos >= 5 else "pendiente"
El detalle que rompe todo
Idempotencia: el mismo evento puede llegar dos veces
Casi ningún proveedor de webhooks garantiza entrega exactamente-una-vez; garantizan "al menos una vez". Si tu procesado no es idempotente (crear un pedido, sumar un stock) un reintento legítimo del proveedor duplica datos. Guardar el event_id externo con una constraint de unicidad es la defensa más simple y efectiva.
| Situación | Qué hacer |
|---|---|
| Evento con ID único del proveedor | Constraint de unicidad sobre ese ID antes de procesar |
| Procesado pesado (crear factura, sincronizar stock) | Encolar y responder rápido al webhook |
| El proveedor no firma sus payloads | Token secreto en la URL o cabecera, nunca confiar a ciegas |
| Consumir una API externa desde Odoo | requests con timeout explícito y reintentos acotados |
Resumen
Un webhook fiable separa recepción de procesado, guarda el evento crudo antes de tocar el negocio, y trata cada evento como si pudiera llegar duplicado. La idempotencia no es un extra: es lo que diferencia una integración robusta de una que un día duplica pedidos sin que nadie lo note a tiempo.