Tambahkan Notifikasi SMS ke Situs Booking AI Anda

Kirim SMS otomatis saat seseorang mengirim permintaan booking.

Roadmap & sumber daya

Baru di sini? Mulai dari sini dulu

Panduan ini meningkatkan proyek Situs Booking AI dengan notifikasi SMS otomatis. Jika Anda belum membuat situs booking, mulai dari panduan utama terlebih dahulu.

Bangun Situs Booking dengan AI

Buka panduan utama

MAIN GUIDE

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

Dapatkan Detail Twilio Anda

Buat akun Twilio

Buka Twilio

https://www.twilio.com/

Dapatkan nilai-nilai ini dari dashboard Twilio Anda:

Nilai Twilio

TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN TWILIO_PHONE_NUMBER

TWILIO_ACCOUNT_SID

Account SID

TWILIO_AUTH_TOKEN

Auth Token

TWILIO_PHONE_NUMBER

Nomor telepon Twilio

Pilih nomor Twilio yang mendukung SMS

Tambahkan Secret di Supabase

Buka Secret Edge Function Supabase

Buka Supabase

https://supabase.com/dashboard/projects

Supabase → Project Settings → Edge Functions → Secrets

Tambahkan secret Twilio yang diperlukan

Secret yang Diperlukan

TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token TWILIO_PHONE_NUMBER=+1234567890

SMS pelanggan diaktifkan secara default. SMS pemilik bersifat opsional dan dinonaktifkan secara default.

Opsional: Kirim SMS ke pemilik bisnis

SMS Pemilik Opsional

SEND_OWNER_SMS=true OWNER_PHONE_NUMBER=+1234567890

Tambahkan ini hanya jika kamu juga ingin pemilik bisnis menerima SMS saat permintaan booking baru dikirim.

Buat Edge Function SMS

Buat Edge Function baru dengan nama persis ini: send-sms-notification

Tinjau template pesan SMS

Salin dan tempel kode fungsi SMS yang dihasilkan

Fungsi Notifikasi 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, }); });

Nama bisnis

Nama Bisnis Anda

Pesan SMS Pelanggan

Halo {{customer_name}}, kami telah menerima permintaan booking {{service_name}} Anda untuk {{appointment_date}} pukul {{appointment_time}}. Kami akan segera menghubungi Anda untuk mengonfirmasi.

Pesan SMS Pemilik

Permintaan booking baru: {{customer_name}} meminta {{service_name}} pada {{appointment_date}} pukul {{appointment_time}}. Telepon: {{customer_phone}}

Opsional: perbarui nama field jika diperlukan

Fungsi SMS ini bekerja dengan field booking umum seperti: full_name, phone, appointment_date, start_time, service_id, dan service_name. Jika proyek Anda menggunakan nama field berbeda, perbarui FIELD_MAP di dalam fungsi agar sesuai dengan tabel booking Anda.

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"], };

Hubungkan Webhook

Pilih tabel booking Anda

Dalam video ini, kami menggunakan: public.appointments Jika proyek Anda menggunakan tabel booking yang berbeda, pilih tabel tersebut.

Buka Database Webhooks Supabase

Supabase → Database → Webhooks → Create webhook

Gunakan pengaturan webhook ini

Pengaturan Webhook

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

Aktifkan hanya event Insert

Uji Notifikasi SMS

Buat booking baru dari situs Anda

Periksa apakah pelanggan menerima SMS

Periksa Log Edge Function Supabase

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

Jika perlu, periksa Log Messaging Twilio

Twilio → Messaging → Logs

Sebelum go-live, periksa aturan SMS untuk negaramu

Persyaratan SMS berbeda menurut negara

Persyaratan SMS dapat berbeda menurut negara tujuan. Sebelum menggunakan ini di produksi, periksa persyaratan pesan Twilio untuk negara targetmu. Misalnya, SMS berbasis aplikasi ke nomor AS mungkin memerlukan registrasi A2P 10DLC.