+12025551234).
Agent Builder. Copy the key — it starts with KEY0…. Store it securely — you won't see it again.
send-smssend-smsAuthorization: Bearer <anon_key> in the header — JWT verification isn't needed.
// ================================================================
// send-sms — Supabase Edge Function
// Sends SMS via Telnyx API v2.
// https://developers.telnyx.com/docs/messaging/messages/send-message
//
// Deploy via Supabase Dashboard:
// Edge Functions → New Function "send-sms" → JWT OFF → paste → Deploy
//
// POST /functions/v1/send-sms
// Headers: { Authorization: Bearer <anon_key> }
// Body:
// Option A (inline test): { telnyx_api_key, telnyx_from_number, to, body }
// Option B (live flow): { account_id, to, body }
// Returns: { ok:true, messageId, to } | { ok:false, error:"..." }
// ================================================================
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const CORS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
function json(data, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: { ...CORS, "Content-Type": "application/json" },
});
}
serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: CORS });
try {
const body = await req.json();
// ── 1. Resolve credentials ──────────────────────────────────
let apiKey = "";
let fromNum = "";
let profileId = "";
if (body.telnyx_api_key) {
// Option A — inline creds (from SMS Settings test panel)
apiKey = String(body.telnyx_api_key);
fromNum = String(body.telnyx_from_number || "");
profileId = String(body.telnyx_messaging_profile_id || "");
} else if (body.account_id) {
// Option B — load stored settings from Supabase DB
const supabase = createClient(
Deno.env.get("SUPABASE_URL"),
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")
);
const { data, error } = await supabase
.from("sms_settings")
.select("*")
.eq("account_id", body.account_id)
.single();
if (error || !data) {
return json({
ok: false,
error: "No SMS settings found. Configure in App Settings → SMS Settings."
});
}
if (!data.sms_enabled) {
return json({
ok: false,
error: "SMS sending is disabled. Enable it in App Settings → SMS Settings."
});
}
if (data.provider !== "telnyx") {
return json({ ok: false, error: `Provider '${data.provider}' is not yet supported.` });
}
apiKey = String(data.telnyx_api_key || "");
fromNum = String(data.telnyx_from_number || "");
profileId = String(data.telnyx_messaging_profile_id || "");
} else {
return json({
ok: false,
error: "Provide telnyx_api_key directly, or account_id to load stored settings."
});
}
// ── 2. Validate required fields ─────────────────────────────
if (!apiKey) return json({ ok: false, error: "Telnyx API key is empty. Check SMS Settings." });
if (!fromNum) return json({ ok: false, error: "Telnyx From Number is empty. Check SMS Settings." });
if (!body.to) return json({ ok: false, error: "'to' phone number is required (E.164 format: +12125551234)." });
if (!body.body && body.body !== 0)
return json({ ok: false, error: "'body' message text is required." });
// Warn if number doesn't look like E.164
const toNum = String(body.to).trim();
if (!toNum.startsWith("+")) {
console.warn("[send-sms] 'to' number may not be E.164 format:", toNum);
}
// ── 3. Build Telnyx API v2 payload ──────────────────────────
// https://developers.telnyx.com/docs/messaging/messages/send-message
const telnyxPayload = {
from: fromNum,
to: toNum,
text: String(body.body),
type: "SMS",
};
// Messaging Profile ID is optional but recommended for production
if (profileId) telnyxPayload.messaging_profile_id = profileId;
// ── 4. Call Telnyx Messages API ─────────────────────────────
console.log(`[send-sms] Sending to ${toNum} from ${fromNum}…`);
const telnyxRes = await fetch("https://api.telnyx.com/v2/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body: JSON.stringify(telnyxPayload),
});
const telnyxData = await telnyxRes.json();
// ── 5. Handle Telnyx response ───────────────────────────────
if (!telnyxRes.ok) {
// Extract the most useful error message from Telnyx error schema
const errDetail = telnyxData?.errors?.[0]?.detail
|| telnyxData?.errors?.[0]?.title
|| telnyxData?.detail
|| `Telnyx API error ${telnyxRes.status}`;
console.error(`[send-sms] ✗ Telnyx error (${telnyxRes.status}):`, errDetail);
return json({ ok: false, error: errDetail }, 500);
}
const messageId = telnyxData?.data?.id || "";
const status = telnyxData?.data?.status || "queued";
console.log(`[send-sms] ✓ Sent to ${toNum} | id=${messageId} | status=${status}`);
return json({ ok: true, messageId, to: toNum, status });
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error("[send-sms] ✗ Uncaught error:", msg);
return json({ ok: false, error: msg }, 500);
}
});
{{variables}} in the SMS body for dynamic content (e.g. Hi {{customerName}}, your booking is confirmed!){{phoneVar}} to route to the customer's number collected earlier in the flowsmsResult) — use it in a Condition node to branch on success/failuremkdir -p supabase/functions/send-sms # Paste the function code (Step 3 above) into: # supabase/functions/send-sms/index.ts
supabase login supabase functions deploy send-sms --no-verify-jwt
Both from and to must be in
E.164 format — a + followed by the country code and number, no spaces or dashes.
Examples: +12125551234 (US), +447911123456 (UK), +61412345678 (AU).
Telnyx will reject numbers that don't start with +.
You can send without a Messaging Profile — just set the from number directly.
However, using a Messaging Profile is recommended for production because it: (1) groups your numbers, (2) enables delivery status webhooks, (3) helps manage rate limits and 10DLC registration for US traffic.
For US domestic messaging, carriers require 10DLC (10-Digit Long Code) registration — you must register your brand and campaign in the Telnyx portal. Unregistered traffic is filtered or blocked by US carriers. This is a carrier regulation, not a Telnyx restriction. Registration takes 1–3 business days. See Telnyx 10DLC docs.
| 40003 / Invalid to | Phone number not in E.164 format or not valid for SMS |
| 40002 / Invalid from | From number not owned by your account or not SMS-capable |
| 40300 / Unauthorized | Invalid API key — check KEY0… prefix, no extra spaces |
| 20001 / Filtered | US traffic filtered — 10DLC registration required for this route |