AI予約サイトにSMS通知を追加する

予約リクエストが届いたら自動でSMSを送ります。

ロードマップと資料

初めての方へ:まずはここから

このガイドでは、AI予約サイトのプロジェクトに自動SMS通知を追加します。まだ予約サイトを作っていない場合は、先にメインのガイドから始めてください。

AIで予約サイトを作る

メインのガイドを開く

MAIN GUIDE

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

Twilioの情報を取得する

Twilioのアカウントを作成する

Twilioを開く

https://www.twilio.com/

これらの値はTwilioのダッシュボードから取得します。

Twilioの設定値

TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN TWILIO_PHONE_NUMBER

TWILIO_ACCOUNT_SID

Account SID

TWILIO_AUTH_TOKEN

Auth Token

TWILIO_PHONE_NUMBER

Twilio の電話番号

SMSに対応したTwilioの電話番号を選ぶ

SupabaseにSecretsを追加する

Supabase Edge FunctionのSecretsを開く

Supabaseを開く

https://supabase.com/dashboard/projects

Supabase → Project Settings → Edge Functions → Secrets

必要なTwilioのsecretを追加する

必要なSecrets

TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token TWILIO_PHONE_NUMBER=+1234567890

お客様へのSMSは初期状態で有効です。オーナーへのSMSは任意で、初期状態では無効です。

任意:オーナーにSMSを送る

オーナー宛SMS(任意)

SEND_OWNER_SMS=true OWNER_PHONE_NUMBER=+1234567890

新しい予約リクエストが届いたときに、オーナーにもSMSを送りたい場合だけ追加してください。

SMS用のEdge Functionを作成する

次の名前でEdge Functionを新規作成します:send-sms-notification

SMSの文面テンプレートを確認する

生成されたSMS関数のコードをコピーして貼り付ける

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

店舗・会社名

あなたの店舗・会社名

お客様向けSMSの文面

{{customer_name}}様、{{appointment_date}} {{appointment_time}}の{{service_name}}のご予約リクエストを承りました。確定のご連絡を追ってお送りいたします。

オーナー向けSMSの文面

新しい予約リクエスト:{{customer_name}}様が{{appointment_date}} {{appointment_time}}に{{service_name}}をリクエストしました。電話:{{customer_phone}}

任意:必要ならフィールド名を更新する

このSMS関数は、次のような一般的な予約フィールドに対応しています。 full_name、phone、appointment_date、start_time、service_id、service_name。 プロジェクトで別のフィールド名を使っている場合は、関数内のFIELD_MAPを予約テーブルに合わせて変更してください。

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

Webhookを接続する

予約用のテーブルを選ぶ

この動画では public.appointments を使っています。 プロジェクトで別の予約テーブルを使っている場合は、そちらを選んでください。

SupabaseのDatabase Webhooksを開く

Supabase → Database → Webhooks → Create webhook

次のWebhook設定を使います

Webhookの設定

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

Insertイベントだけを有効にする

SMS通知をテストする

サイトから新しい予約を入れてみる

お客様にSMSが届いたか確認する

Supabase Edge Functionのログを確認する

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

必要に応じてTwilio Messagingのログを確認する

Twilio → Messaging → Logs

公開する前に、お住まいの国のSMSルールを確認してください

SMSの要件は国によって異なります

SMSの要件は送信先の国によって異なります。本番で使う前に、対象の国におけるTwilioのメッセージ要件を確認してください。たとえば、米国の番号へアプリからSMSを送る場合、A2P 10DLCの登録が必要になることがあります。