Crear Páginas Web con Lógica de NegocioBeta

0%
Bloque Técnico-Operativo

Módulo 05

Integraciones: email, WhatsApp y APIs externas

2hResendWhatsApp Business APIVercel Cron

Consumir APIs externas sin romper tu flujo, emails transaccionales con Resend, recordatorios por WhatsApp Business API y tareas programadas con cron.

Recursos descargables

Catálogo de Integraciones para tu Negocio

Comparativa de servicios de email, WhatsApp y cron con límites gratuitos, plantilla de secuencia de notificaciones y plantilla de WhatsApp lista para aprobar

Markdown
En este módulo
01
APIs externas: cómo tu app habla con otros servicios El patrón para consumir cualquier API sin romper tu negocio cuando ellas fallen

ReservaYa ya cobra, pero Julián sigue haciendo trabajo de robot: confirma cada cita por WhatsApp a mano y llama el día antes para recordar. Son 40 mensajes al día. Y cuando se le olvida uno, esa silla se queda vacía.

Todo lo que Julián hace copiando y pegando mensajes, una API lo hace en milisegundos. Integrar es delegar: email, WhatsApp y recordatorios pasan a ser código.

1.1. Anatomía de una llamada a API externa

Ya consumiste APIs sin notarlo: Supabase y Wompi lo son. El patrón general de cualquier integración desde el servidor:

// El esqueleto de TODA integración externa
async function llamarApiExterna() {
  const respuesta = await fetch("https://api.servicio.com/v1/recurso", {
    method: "POST",
    headers: {
      // 1. Autenticación: la API key SIEMPRE desde variables de entorno
      Authorization: `Bearer ${process.env.SERVICIO_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ dato: "valor" }),
  });

  // 2. Manejo de errores: las APIs externas FALLAN — planéalo
  if (!respuesta.ok) {
    const error = await respuesta.text();
    console.error(`API servicio falló (${respuesta.status}):`, error);
    return null; // el negocio decide qué hacer con el fallo
  }

  return respuesta.json();
}
La regla de las integraciones

Una integración externa nunca debe tumbar tu flujo principal. Si el email de confirmación falla, la cita igual queda confirmada (y el envío se reintenta). Si el flujo fuera al revés — cita condicionada al email — un fallo de un tercero paralizaría tu negocio.

ConceptoQué significaQué haces al respecto
API keyTu credencial ante el servicioVariable de entorno de servidor, jamás en el código ni en el navegador
Rate limitMáximo de peticiones por minutoRespeta el límite; agrupa envíos; maneja el error 429
TimeoutLa API no respondeDefine un tiempo máximo y un plan B (cola de reintentos)
SandboxEntorno de prueba del servicioDesarrolla siempre ahí; producción solo con el flujo probado
02
Emails transaccionales con Resend La confirmación de cita que llega a la bandeja de entrada, no a spam

Un email transaccional responde a una acción del usuario: confirmación de cita, recibo de pago, recuperación de clave. Se envía con servicios especializados (Resend, SendGrid, Brevo) — nunca desde tu Gmail personal, que no está hecho para eso y termina en spam.

2.1. Configurar Resend

  1. Crea la cuenta y la API key

    En resend.com el plan gratuito da 100 emails/día — suficiente para la barbería. Copia la API key a .env.local como RESEND_API_KEY.

  2. Instala el SDK

    npm install resend

  3. Verifica tu dominio (en producción)

    Para enviar desde citas@donbarbas.co agregarás 3 registros DNS (SPF, DKIM) en el módulo 6. Mientras tanto, Resend te presta un remitente de pruebas.

2.2. El email de confirmación de cita

// lib/notificaciones/email.ts
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

interface DatosConfirmacion {
  para: string;
  nombreCliente: string;
  servicio: string;
  barbero: string;
  fecha: string;   // ya formateada: "jueves 9 de julio, 3:00 pm"
}

export async function enviarConfirmacionCita(datos: DatosConfirmacion) {
  try {
    await resend.emails.send({
      from: "Don Barbas <citas@donbarbas.co>",
      to: datos.para,
      subject: `✂️ Cita confirmada: ${datos.servicio} — ${datos.fecha}`,
      html: `
        <div style="font-family: sans-serif; max-width: 480px; margin: 0 auto;">
          <h2>¡Listo, ${datos.nombreCliente}!</h2>
          <p>Tu cita en <strong>Don Barbas</strong> quedó confirmada:</p>
          <ul>
            <li><strong>Servicio:</strong> ${datos.servicio}</li>
            <li><strong>Barbero:</strong> ${datos.barbero}</li>
            <li><strong>Fecha:</strong> ${datos.fecha}</li>
            <li><strong>Dirección:</strong> Cra 35 #8A-15, Medellín</li>
          </ul>
          <p>Si no puedes asistir, cancela con 2 horas de anticipación
          para conservar tu anticipo.</p>
        </div>
      `,
    });
    return { ok: true };
  } catch (error) {
    // La cita NO se cae porque el email falle
    console.error("Fallo envío de confirmación:", error);
    return { ok: false };
  }
}

¿Y dónde se llama? En el webhook del módulo 4, justo después de confirmar la cita — el momento en que el negocio sabe que el pago es real:

// app/api/webhooks/wompi/route.ts (fragmento nuevo)
if (nuevoEstado === "APROBADO") {
  await supabaseAdmin.from("citas")
    .update({ estado: "confirmada" })
    .eq("id", pago.cita_id);

  // Integración: confirmar por email (sin bloquear la respuesta del webhook)
  const { data: detalle } = await supabaseAdmin
    .from("citas")
    .select("inicia_en, perfiles(nombre), servicios(nombre), barberos(nombre)")
    .eq("id", pago.cita_id)
    .single();

  if (detalle) {
    enviarConfirmacionCita({ /* ...mapear campos... */ }).catch(console.error);
  }
}
03
WhatsApp Business API: donde tus clientes sí leen Recordatorios automáticos por el canal número 1 de Latinoamérica

En Latinoamérica el email se ignora, pero WhatsApp se lee en minutos. La WhatsApp Business Platform (Cloud API de Meta) permite enviar mensajes desde código. Tiene reglas propias:

Regla 1
Plantillas aprobadas
Para iniciar una conversación necesitas una plantilla pre-aprobada por Meta (ej: "recordatorio_cita"). Se aprueban en horas desde el WhatsApp Manager.
Regla 2
Ventana de 24 horas
Si el cliente te escribe, puedes responder texto libre durante 24 h. Pasado ese tiempo, de nuevo solo plantillas.
Regla 3
Número dedicado
El número de la API no puede usarse en la app normal de WhatsApp. Registra un número nuevo o migra el del negocio con cuidado.

3.1. Enviar el recordatorio de cita

// lib/notificaciones/whatsapp.ts
// WhatsApp Cloud API de Meta — plantilla "recordatorio_cita" ya aprobada

export async function enviarRecordatorioWhatsApp(
  celular: string,      // formato internacional sin +: "573001234567"
  nombre: string,
  fecha: string,
  barbero: string
) {
  const url = `https://graph.facebook.com/v20.0/${process.env.WHATSAPP_PHONE_ID}/messages`;

  const respuesta = await fetch(url, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.WHATSAPP_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      messaging_product: "whatsapp",
      to: celular,
      type: "template",
      template: {
        name: "recordatorio_cita",       // plantilla aprobada en Meta
        language: { code: "es_CO" },
        components: [{
          type: "body",
          // Variables {{1}}, {{2}}, {{3}} de la plantilla
          parameters: [
            { type: "text", text: nombre },
            { type: "text", text: fecha },
            { type: "text", text: barbero },
          ],
        }],
      },
    }),
  });

  if (!respuesta.ok) {
    console.error("WhatsApp falló:", await respuesta.text());
    return { ok: false };
  }
  return { ok: true };
}
El plan B sin API: el enlace wa.me

Si el papeleo de Meta te frena hoy, existe un atajo digno: el botón "Confirmar por WhatsApp" con https://wa.me/57300XXXXXXX?text=Hola,%20confirmo%20mi%20cita%20RSV-1234 abre el chat del negocio con mensaje pre-escrito — el cliente inicia la conversación y no necesitas API. Muchos negocios arrancan así y migran a la API cuando el volumen lo justifica.

04
Notificaciones programadas: el negocio que trabaja de noche Cron jobs para recordar citas y liberar horarios abandonados

Dos reglas de negocio de ReservaYa no las dispara ningún usuario — las dispara el reloj:

  • Recordatorio: 24 horas antes de cada cita confirmada, enviar WhatsApp.
  • Liberación: las citas en pendiente_pago con más de 15 minutos se cancelan y el horario vuelve a estar disponible.

Para esto existen los cron jobs: tareas que corren en un horario fijo. En Vercel se declaran en vercel.json y llaman a una ruta de tu app:

// vercel.json — en la raíz del proyecto
{
  "crons": [
    { "path": "/api/cron/recordatorios", "schedule": "0 * * * *" },
    { "path": "/api/cron/liberar-citas", "schedule": "*/15 * * * *" }
  ]
}
// app/api/cron/recordatorios/route.ts
import { NextResponse } from "next/server";
import { createClient } from "@supabase/supabase-js";
import { enviarRecordatorioWhatsApp } from "@/lib/notificaciones/whatsapp";

const supabaseAdmin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

export async function GET(request: Request) {
  // Solo Vercel Cron puede invocar esta ruta
  if (request.headers.get("authorization") !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: "no autorizado" }, { status: 401 });
  }

  // Citas confirmadas que empiezan entre 23 y 25 horas desde ahora
  // y que aún no tienen recordatorio enviado
  const en23h = new Date(Date.now() + 23 * 3600_000).toISOString();
  const en25h = new Date(Date.now() + 25 * 3600_000).toISOString();

  const { data: citas } = await supabaseAdmin
    .from("citas")
    .select("id, inicia_en, perfiles(nombre, telefono), barberos(nombre)")
    .eq("estado", "confirmada")
    .is("recordatorio_enviado_en", null)
    .gte("inicia_en", en23h)
    .lte("inicia_en", en25h);

  for (const cita of citas ?? []) {
    const perfil = cita.perfiles as any;
    if (!perfil?.telefono) continue;

    const { ok } = await enviarRecordatorioWhatsApp(
      `57${perfil.telefono}`,
      perfil.nombre,
      new Date(cita.inicia_en).toLocaleString("es-CO"),
      (cita.barberos as any)?.nombre ?? "tu barbero"
    );

    // Marcamos el envío: idempotencia también en los crons
    if (ok) {
      await supabaseAdmin.from("citas")
        .update({ recordatorio_enviado_en: new Date().toISOString() })
        .eq("id", cita.id);
    }
  }

  return NextResponse.json({ procesadas: citas?.length ?? 0 });
}
⚙ La columna que hace el cron idempotente

Agrega recordatorio_enviado_en timestamptz a la tabla citas (alter table citas add column recordatorio_enviado_en timestamptz;). Sin ella, cada corrida del cron reenviaría los mismos recordatorios. Es el mismo principio del webhook del módulo 4: toda tarea automática debe poder correr dos veces sin duplicar efectos.

✗ Errores comunes con integraciones
  • Bloquear el flujo principal esperando la integración. Solución: la cita se confirma primero; el email/WhatsApp se dispara después y sus fallos se registran, no se propagan.
  • Enviar marketing por la plantilla transaccional. Solución: Meta suspende números que abusan. Las plantillas transaccionales son para eso: transacciones.
  • Exponer una ruta de cron sin protección. Solución: verifica CRON_SECRET en el header. Una ruta de cron abierta es una API pública que cualquiera puede disparar.
  • Ignorar los fallos silenciosos. Solución: registra cada envío fallido (tabla notificaciones_fallidas o al menos console.error) y revisa los logs semanalmente.
✏ Ejercicio: recordatorios del consultorio odontológico

La Dra. Sandra tiene un consultorio en Guadalajara. Sus datos: 30% de inasistencia en citas de valoración (gratuitas) y 10% en tratamientos (pagados). Cada silla vacía cuesta $800 MXN.

  1. Diseña la secuencia de notificaciones para cada tipo de cita: qué canal (email/WhatsApp), cuándo (al agendar, 48 h antes, 3 h antes) y qué dice cada mensaje.
  2. Escribe la plantilla de WhatsApp "recordatorio_valoracion" con sus variables {{1}}, {{2}}, {{3}}, lista para enviar a aprobación de Meta.
  3. Define la regla de negocio que reduce la inasistencia de valoraciones: ¿pedir confirmación con botón? ¿liberar el cupo si no confirma 24 h antes? Escríbela como "si X, entonces Y".

El catálogo de integraciones del módulo (recursos descargables) incluye precios y límites de cada servicio.

4.1. Mini-proyecto del módulo

🔨
ReservaYa · Entrega 5

Tu app ahora se comunica sola: email de confirmación vía Resend disparado desde el webhook de pago aprobado, recordatorio de WhatsApp (Cloud API o, como plan B, botón wa.me), cron de recordatorios cada hora con su columna de idempotencia, y cron que libera citas no pagadas cada 15 minutos. Haz una reserva de prueba con tu propio correo y verifica que el email llega después de aprobar el pago sandbox.

✓ Lo que aprendiste en este módulo
  • Toda integración sigue el mismo esqueleto: fetch + API key en variable de entorno + manejo explícito del fallo.
  • Las integraciones nunca bloquean el negocio: la cita se confirma aunque el email falle; el fallo se registra y reintenta.
  • WhatsApp API tiene reglas propias: plantillas aprobadas para iniciar, ventana de 24 h para conversar, y wa.me como plan B sin papeleo.
  • El reloj también dispara lógica de negocio: cron jobs protegidos con secreto e idempotentes, para recordar citas y liberar horarios.