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.