Tutorial · 12 min

Cómo automatizar pedidos por WhatsApp
en un restaurante de Lima.

Te cuento la arquitectura mínima viable que usé para Mar y Selva: cómo entran los pedidos, cómo la IA extrae los datos del mensaje, cómo se valida la cobertura por distrito y cómo termina la comanda en cocina sin que nadie copie y pegue a mano. Y al final, los tres errores concretos que cometí — para que tú no los repitas.

Stack
Twilio + n8n + Supabase + Claude Haiku
Dificultad
Intermedio — necesitas saber qué es un webhook
Tiempo de lectura
12 minutos
Caso real
Mar y Selva, Surquillo
El problema

Un solo número de WhatsApp, dos turnos, y los pedidos se perdían entre saludos.

El restaurante recibe pedidos por WhatsApp desde el almuerzo hasta cierre. El número lo manejaba Gabriela, la cajera, en paralelo con la caja física, la atención en sala y las reservas. En horas pico llegaban diez mensajes simultáneos: unos pidiendo carta, otros confirmando una reserva, otros mandando una foto del Yape para pagar un delivery.

Los problemas eran predecibles. Pedidos que entraban a las 9:45pm y nadie respondía hasta el día siguiente. Direcciones mal copiadas que terminaban con el mensajero llamando al cliente desde la otra punta del distrito. Yapes confirmados sin que el ticket llegara nunca a cocina. Y el restaurante perdiendo plata sin saber por qué.

El primer instinto del dueño fue contratar a alguien más. Lo descartamos. El problema no era de personal: era que el flujo de pedido no estaba estructurado en ningún lado, vivía en la cabeza de Gabriela. Antes de meter más humanos al caos, había que digitalizar el caos.

Arquitectura mínima viable

Cuatro piezas que hacen todo el trabajo. Nada más.

El stack se reduce a cuatro componentes, y todos hablan el mismo idioma vía webhooks. Lo dejé deliberadamente simple porque iba a tener que mantenerlo yo mismo durante meses.

Twilio WhatsApp Business API. Es el puente entre el WhatsApp del cliente y el resto del sistema. Cada mensaje entrante pega contra un webhook de n8n en menos de un segundo. Empecé con el sandbox (whatsapp:+14155238886) y migré a número productivo cuando el flujo ya estaba estable.

n8n como router. Es el cerebro. Recibe el webhook, identifica al cliente en Supabase, lo manda al workflow correcto según intención, y orquesta todas las respuestas. Self-hosted en mi máquina al inicio, después al VPS. Quince workflows en total, pero para pedidos delivery solo importan cuatro.

Supabase como base de datos. Tablas para clientes, menú con alérgenos y variantes, pedidos delivery, y zonas de cobertura. Postgres puro con HTTP node desde n8n. Sin ORM, sin abstracciones — query directa.

Claude Haiku 4.5 como extractor. Aquí está la diferencia con un bot tradicional de menú-por-botones. En vez de obligar al cliente a navegar un árbol, lo dejas escribir como escribiría a un humano — "quiero 2 patarashcas mixtas y 1 chicha morada, mando a Tomás Marsano 1340" — y el modelo te devuelve un JSON estructurado con items, cantidades, dirección y método de pago.

Flujo paso a paso

De "hola, quiero pedir" a comanda en cocina, sin tocar el teclado.

El flujo completo son seis pasos. Cada paso es un nodo de n8n encadenado con el siguiente. Te lo cuento en el orden en que se ejecuta:

  1. Webhook recibe el mensaje. Twilio te manda el body como application/x-www-form-urlencoded. El campo importante es WaId (teléfono sin el +) y Body (texto). Si es audio, llega MediaUrl0 y se manda a Whisper para transcribir.
  2. Identificar al cliente. HTTP node a Supabase: GET /customers?phone=eq.%2B{{ $json.WaId }}. Si no existe, lo creo en ese mismo nodo. Si existe, traigo su conversation_history y sus alérgenos.
  3. Router IA detecta intención. Claude Haiku recibe el mensaje + historial + menú compacto y devuelve uno de seis valores: reserva, pedido_delivery, consulta_menu, queja, cancelacion, general. Si es pedido_delivery, sigo en este workflow. Si es otra cosa, salto a otro.
  4. Extracción estructurada. Segunda llamada a Claude pidiéndole un JSON específico: { items: [{nombre, cantidad, variante}], direccion, metodo_pago, notas }. Aquí es donde el modelo brilla — el cliente escribió "una patarashca mixta sin culantro y un tacacho con cecina, pago con Yape" y tú recibes un objeto listo para insertar.
  5. Validar cobertura y precio. La dirección pasa por Google Maps Distance Matrix contra la dirección del local. Si el distrito está en la tabla delivery_zones, calculo el precio del delivery. Si no, le contesto educadamente que aún no llegamos a esa zona. Antes de 6pm la zona de cobertura es amplia; después se reduce — eso lo decide el restaurante, no yo.
  6. Confirmar al cliente y mandar comanda. Le mando un mensaje resumiendo el pedido, total y tiempo estimado. Si confirma, inserto el pedido en delivery_orders con estado pendiente_pago, genero el link de pago con Culqi, y cuando Culqi confirma el cobro, el pedido pasa a la cocina — vía API del POS si está disponible, o vía un mensaje formateado al canal Alertas Delivery de Chatwoot si no.

Eso es todo. El cliente recibe respuestas en menos de cuatro segundos en cualquier paso. Gabriela solo interviene si algo se sale del flujo — un cambio de dirección a último momento, una queja, un pedido de un influencer al que hay que tratar distinto.

Errores que cometí

Tres cosas que se rompieron en producción, y cómo las arreglé.

Si estás construyendo algo parecido, ahórrate estos tres dolores de cabeza. Los descubrí en producción, con clientes reales esperando respuesta.

1. URL encoding del + en queries a Supabase. Los teléfonos en Perú empiezan con +51. Si haces GET /customers?phone=eq.+51998344450, Postgrest interpreta el + como un espacio y nunca encuentra al cliente. Hay que encodearlo como %2B. Mi query final quedó phone=eq.%2B{{ $json.WaId }}. Pasé tres horas pensando que era un bug del HTTP node.

2. El bug del Switch v3 con caseSensitive. En n8n v3, el nodo Switch tiene una opción caseSensitive que en ciertas versiones simplemente no funciona — la comparación es siempre case-sensitive aunque la apagues. Me partió el router porque Claude a veces devolvía Pedido_Delivery y a veces pedido_delivery. Solución: forzar .toLowerCase() en un Code node previo, o reemplazar el Switch por uno recreado desde cero — a veces los nodos viejos cargan con el bug y los nuevos no.

3. No validar la dirección antes de cobrar. En la primera versión cobraba con Culqi y después validaba cobertura. Mal. Tuve dos clientes que pagaron y luego les tuve que devolver la plata porque vivían fuera de zona. Ahora la validación de Google Maps va antes del link de pago, y el link de pago solo se genera si la dirección está dentro del polígono. Suena obvio escrito, pero en el calor de armar el flujo es fácil saltarse pasos.

Bonus: cuando estés construyendo el prompt del router, dale al modelo el menú completo con ingredientes y alérgenos — no solo nombres. Los clientes preguntan "¿el tacacho tiene gluten?" mucho más seguido de lo que crees, y si el modelo solo ve nombres de platos, alucina.

¿Quieres ver el sistema completo en producción?

Este post cubre el flujo de pedidos. El sistema real de Mar y Selva tiene además reservas, recordatorios, panel admin, broadcast semanal y resumen diario para el dueño. Si quieres ver la arquitectura completa o calcular cuánto te ahorraría algo así, ahí abajo tienes los dos siguientes pasos.