Añade notificaciones SMS a tu sitio de reservas con IA

Envía un SMS automático cuando alguien envía una solicitud de reserva.

Hoja de ruta y recursos

¿Nuevo? Empieza aquí primero

Esta guía mejora el proyecto del sitio de reservas con IA con notificaciones SMS automáticas. Si todavía no has creado el sitio de reservas, empieza primero por la guía principal.

Construye un sitio de reservas con IA

Abre la guía principal

MAIN GUIDE

https://profitstudio.app/video/build-booking-website-ai

Obtén tus credenciales de Twilio

Crea una cuenta de Twilio

Abrir Twilio

https://www.twilio.com/

Obtén estos valores desde tu panel de Twilio:

Valores de Twilio

TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN TWILIO_PHONE_NUMBER

TWILIO_ACCOUNT_SID

Account SID

TWILIO_AUTH_TOKEN

Auth Token

TWILIO_PHONE_NUMBER

Número de teléfono de Twilio

Elige un número de Twilio que admita SMS

Añade los secretos en Supabase

Abrir los secretos de Edge Functions en Supabase

Abrir Supabase

https://supabase.com/dashboard/projects

Supabase → Project Settings → Edge Functions → Secrets

Añade los secretos de Twilio requeridos

Secretos requeridos

TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token TWILIO_PHONE_NUMBER=+1234567890

El SMS al cliente está activado por defecto. El SMS al propietario es opcional y está desactivado por defecto.

Opcional: Enviar SMS al propietario del negocio

SMS al propietario (opcional)

SEND_OWNER_SMS=true OWNER_PHONE_NUMBER=+1234567890

Añádelos solo si también quieres que el propietario del negocio reciba un SMS cuando se envíe una nueva solicitud de reserva.

Crea la Edge Function de SMS

Crea una nueva Edge Function con este nombre exacto: send-sms-notification

Revisa la plantilla del mensaje SMS

Copia y pega el código de la función SMS generada

Función de notificación SMS

// supabase/functions/send-sms-notification/index.ts // // Sends SMS notifications when a new booking request is created. // Default: sends booking request SMS to the customer. // Optional: also sends SMS notification to the business owner. // // Required Supabase Secrets: // TWILIO_ACCOUNT_SID // TWILIO_AUTH_TOKEN // TWILIO_PHONE_NUMBER // // Optional Owner SMS Secrets: // SEND_OWNER_SMS=true // OWNER_PHONE_NUMBER=+1234567890 const CORS_HEADERS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; // ===== CUSTOMIZE THESE VALUES ===== const BUSINESS_NAME = "Your Business Name"; const CUSTOMER_SMS_TEMPLATE = "Hi {{customer_name}}, we received your {{service_name}} booking request for {{appointment_date}} at {{appointment_time}}. We'll contact you soon to confirm."; const OWNER_SMS_TEMPLATE = "New booking request: {{customer_name}} requested {{service_name}} on {{appointment_date}} at {{appointment_time}}. Phone: {{customer_phone}}"; // Default booking field names used in this guide. // If your project uses different field names, update them here. const FIELD_MAP = { customerName: ["full_name", "customer_name", "name"], customerPhone: ["phone", "phone_number", "customer_phone"], appointmentDate: ["appointment_date", "date", "booking_date"], appointmentTime: ["start_time", "appointment_time", "time"], serviceName: ["service_name", "service"], serviceId: ["service_id"], }; // ===== END CUSTOMIZATION ===== function json(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, }); } function getEnvFlag(name: string, defaultValue = false): boolean { const value = Deno.env.get(name); if (!value) return defaultValue; return value.toLowerCase() === "true"; } function getField(record: Record<string, unknown>, keys: string[]): string { for (const key of keys) { const value = record[key]; if (typeof value === "string" && value.trim()) { return value.trim(); } } return ""; } function formatTime(time: string): string { if (!time) return ""; return time.length >= 5 ? time.slice(0, 5) : time; } function renderTemplate( template: string, values: Record<string, string>, ): string { return template.replace(/\{\{(.*?)\}\}/g, (_, key) => { return values[key.trim()] ?? ""; }); } async function fetchServiceName( serviceId: string, supabaseUrl: string, serviceRoleKey: string, ): Promise<string> { try { const url = `${supabaseUrl}/rest/v1/services?id=eq.${encodeURIComponent( serviceId, )}&select=name`; const response = await fetch(url, { headers: { apikey: serviceRoleKey, Authorization: `Bearer ${serviceRoleKey}`, }, }); if (!response.ok) return ""; const rows = await response.json(); return rows?.[0]?.name ?? ""; } catch { return ""; } } async function sendTwilioSms(options: { accountSid: string; authToken: string; from: string; to: string; body: string; }) { const { accountSid, authToken, from, to, body } = options; const url = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`; const form = new URLSearchParams({ From: from, To: to, Body: body, }); const response = await fetch(url, { method: "POST", headers: { Authorization: "Basic " + btoa(`${accountSid}:${authToken}`), "Content-Type": "application/x-www-form-urlencoded", }, body: form.toString(), }); const data = await response.json().catch(() => null); return { ok: response.ok, status: response.status, data, }; } Deno.serve(async (req) => { if (req.method === "OPTIONS") { return new Response("ok", { headers: CORS_HEADERS }); } if (req.method !== "POST") { return json({ success: false, error: "Method not allowed" }, 405); } const accountSid = Deno.env.get("TWILIO_ACCOUNT_SID"); const authToken = Deno.env.get("TWILIO_AUTH_TOKEN"); const twilioPhoneNumber = Deno.env.get("TWILIO_PHONE_NUMBER"); const sendCustomerSms = getEnvFlag("SEND_CUSTOMER_SMS", true); const sendOwnerSms = getEnvFlag("SEND_OWNER_SMS", false); const ownerPhoneNumber = Deno.env.get("OWNER_PHONE_NUMBER"); if (!accountSid || !authToken || !twilioPhoneNumber) { return json( { success: false, error: "Missing Twilio secrets. Please add TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER.", }, 500, ); } let payload; try { payload = await req.json(); } catch { return json({ success: false, error: "Invalid JSON body" }, 400); } const record = payload?.record; if (!record) { return json({ success: false, error: "Missing record in payload" }, 400); } const customerName = getField(record, FIELD_MAP.customerName) || "Customer"; const customerPhone = getField(record, FIELD_MAP.customerPhone); const appointmentDate = getField(record, FIELD_MAP.appointmentDate) || "your selected date"; const appointmentTime = formatTime(getField(record, FIELD_MAP.appointmentTime)) || "your selected time"; let serviceName = getField(record, FIELD_MAP.serviceName) || "your service"; const serviceId = getField(record, FIELD_MAP.serviceId); const supabaseUrl = Deno.env.get("SUPABASE_URL"); const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); if (serviceId && supabaseUrl && serviceRoleKey) { const fetchedServiceName = await fetchServiceName( serviceId, supabaseUrl, serviceRoleKey, ); if (fetchedServiceName) { serviceName = fetchedServiceName; } } const templateValues = { business_name: BUSINESS_NAME, customer_name: customerName, customer_phone: customerPhone || "Not provided", service_name: serviceName, appointment_date: appointmentDate, appointment_time: appointmentTime, }; const results = { customer_sms: "not_sent", owner_sms: "not_sent", errors: [] as unknown[], }; if (sendCustomerSms) { if (!customerPhone) { results.customer_sms = "skipped_missing_customer_phone"; } else { const customerMessage = renderTemplate( CUSTOMER_SMS_TEMPLATE, templateValues, ); const customerResult = await sendTwilioSms({ accountSid, authToken, from: twilioPhoneNumber, to: customerPhone, body: customerMessage, }); if (!customerResult.ok) { results.customer_sms = "failed"; results.errors.push({ type: "customer_sms_failed", status: customerResult.status, twilio: customerResult.data, }); } else { results.customer_sms = "sent"; } } } if (sendOwnerSms) { if (!ownerPhoneNumber) { results.owner_sms = "skipped_missing_owner_phone"; } else { const ownerMessage = renderTemplate( OWNER_SMS_TEMPLATE, templateValues, ); const ownerResult = await sendTwilioSms({ accountSid, authToken, from: twilioPhoneNumber, to: ownerPhoneNumber, body: ownerMessage, }); if (!ownerResult.ok) { results.owner_sms = "failed"; results.errors.push({ type: "owner_sms_failed", status: ownerResult.status, twilio: ownerResult.data, }); } else { results.owner_sms = "sent"; } } } if (results.errors.length > 0) { return json( { success: false, message: "One or more SMS messages failed.", results, }, 502, ); } return json({ success: true, message: "SMS notification process completed.", results, }); });

Nombre del negocio

El nombre de tu negocio

Mensaje SMS al cliente

Hola {{customer_name}}, hemos recibido tu solicitud de reserva de {{service_name}} para el {{appointment_date}} a las {{appointment_time}}. Te contactaremos pronto para confirmarla.

Mensaje SMS al propietario

Nueva solicitud de reserva: {{customer_name}} solicitó {{service_name}} el {{appointment_date}} a las {{appointment_time}}. Teléfono: {{customer_phone}}

Opcional: actualiza los nombres de los campos si es necesario

Esta función SMS funciona con campos de reserva comunes como: full_name, phone, appointment_date, start_time, service_id y service_name. Si tu proyecto usa nombres de campo distintos, actualiza FIELD_MAP dentro de la función para que coincida con tu tabla de reservas.

FIELD_MAP

const FIELD_MAP = { customerName: ["full_name", "customer_name", "name"], customerPhone: ["phone", "phone_number", "customer_phone"], appointmentDate: ["appointment_date", "date", "booking_date"], appointmentTime: ["start_time", "appointment_time", "time"], serviceName: ["service_name", "service"], serviceId: ["service_id"], };

Conecta el Webhook

Elige tu tabla de reservas

En este vídeo usamos: public.appointments Si tu proyecto usa una tabla de reservas diferente, elige esa tabla.

Ve a los Webhooks de la base de datos de Supabase

Supabase → Database → Webhooks → Create webhook

Usa esta configuración de webhook

Configuración del webhook

Table: public.appointments Event: Insert Type: Supabase Edge Functions Function: send-sms-notification Method: POST

Activa solo el evento Insert

Prueba la notificación SMS

Crea una nueva reserva desde tu sitio web

Verifica si el cliente recibió el SMS

Revisa los logs de Edge Function en Supabase

Supabase → Edge Functions → send-sms-notification → Logs

Si es necesario, revisa los logs de Messaging en Twilio

Twilio → Messaging → Logs

Antes de pasar a producción, revisa las reglas de SMS de tu país

Los requisitos de SMS varían según el país

Los requisitos de SMS pueden variar según el país de destino. Antes de usar esto en producción, revisa los requisitos de mensajería de Twilio para tu país objetivo. Por ejemplo, los SMS enviados desde aplicaciones a números de EE. UU. pueden requerir el registro A2P 10DLC.