Thêm thông báo SMS cho website đặt lịch AI của bạn
Gửi SMS tự động khi có người gửi yêu cầu đặt lịch.
Lộ trình & tài nguyên
Mới đến đây? Hãy bắt đầu từ chỗ này trước
Hướng dẫn này nâng cấp dự án Website đặt lịch AI với thông báo SMS tự động. Nếu bạn chưa xây dựng website đặt lịch, hãy bắt đầu từ hướng dẫn chính trước.
Xây dựng website đặt lịch bằng AI
Mở hướng dẫn chính
MAIN GUIDE
https://profitstudio.app/video/build-booking-website-ai
Lấy thông tin Twilio của bạn
Tạo tài khoản Twilio
Mở Twilio
https://www.twilio.com/
Lấy các giá trị này từ bảng điều khiển Twilio của bạn:
Các giá trị của Twilio
TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN TWILIO_PHONE_NUMBER
TWILIO_ACCOUNT_SID
Account SID
TWILIO_AUTH_TOKEN
Auth Token
TWILIO_PHONE_NUMBER
Số điện thoại Twilio
Chọn một số Twilio có hỗ trợ SMS
Thêm Secrets trong Supabase
Mở Supabase Edge Function Secrets
Mở Supabase
https://supabase.com/dashboard/projects
Supabase → Project Settings → Edge Functions → Secrets
Thêm các secret Twilio bắt buộc
Các secret bắt buộc
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token TWILIO_PHONE_NUMBER=+1234567890
SMS gửi khách hàng được bật mặc định. SMS gửi chủ doanh nghiệp là tùy chọn và mặc định bị tắt.
Tùy chọn: Gửi SMS cho chủ doanh nghiệp
SMS gửi chủ doanh nghiệp (tùy chọn)
SEND_OWNER_SMS=true OWNER_PHONE_NUMBER=+1234567890
Chỉ thêm những mục này nếu bạn cũng muốn chủ doanh nghiệp nhận SMS khi có yêu cầu đặt lịch mới.
Tạo Edge Function gửi SMS
Tạo một Edge Function mới với đúng tên này: send-sms-notification
Xem lại mẫu nội dung tin nhắn SMS
Sao chép và dán đoạn mã hàm SMS đã được tạo
Hàm gửi thông báo 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, }); });
Tên doanh nghiệp
Tên doanh nghiệp của bạn
Nội dung SMS gửi khách hàng
Chào {{customer_name}}, chúng tôi đã nhận được yêu cầu đặt lịch {{service_name}} của bạn vào {{appointment_date}} lúc {{appointment_time}}. Chúng tôi sẽ liên hệ với bạn sớm để xác nhận.
Nội dung SMS gửi chủ doanh nghiệp
Yêu cầu đặt lịch mới: {{customer_name}} đã đặt {{service_name}} vào {{appointment_date}} lúc {{appointment_time}}. Điện thoại: {{customer_phone}}
Tùy chọn: cập nhật tên các trường nếu cần
Hàm SMS này hoạt động với các trường đặt lịch phổ biến như: full_name, phone, appointment_date, start_time, service_id và service_name. Nếu dự án của bạn dùng tên trường khác, hãy cập nhật FIELD_MAP bên trong hàm cho khớp với bảng đặt lịch của bạn.
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"], };
Kết nối Webhook
Chọn bảng bookings của bạn
Trong video này, chúng ta dùng: public.appointments Nếu dự án của bạn dùng một bảng đặt lịch khác, hãy chọn bảng đó.
Vào mục Database Webhooks của Supabase
Supabase → Database → Webhooks → Create webhook
Dùng các thiết lập webhook này
Thiết lập webhook
Table: public.appointments Event: Insert Type: Supabase Edge Functions Function: send-sms-notification Method: POST
Chỉ bật sự kiện Insert
Kiểm thử thông báo SMS
Tạo một lịch đặt mới từ website của bạn
Kiểm tra xem khách hàng đã nhận được SMS chưa
Kiểm tra Logs của Supabase Edge Function
Supabase → Edge Functions → send-sms-notification → Logs
Kiểm tra Twilio Messaging Logs nếu cần
Twilio → Messaging → Logs
Trước khi chạy thật, hãy kiểm tra quy định về SMS tại quốc gia của bạn
Yêu cầu về SMS khác nhau tùy quốc gia
Yêu cầu về SMS có thể khác nhau tùy quốc gia nhận. Trước khi dùng ở môi trường thật, hãy kiểm tra các yêu cầu về nhắn tin của Twilio cho quốc gia bạn nhắm tới. Ví dụ, SMS từ ứng dụng gửi tới số điện thoại Mỹ có thể cần đăng ký A2P 10DLC.