I used up using an edge function in Supabase which is fine with GPT’s help - make sure you have the sendgrid key in the variables. If you’re having trouble setting this up in the UI, delete and re-try or use the CLI.
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
console.log("Edge Function 'send-email' is running...");
const SENDGRID_API_KEY = Deno.env.get('SENDGRID_API_KEY');
const FROM_EMAIL = "no-reply@domain.com"; // Must be verified in SendGrid
function corsHeaders() {
return {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization"
};
}
serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders() });
}
try {
const { to, subject, text, html, template_id, dynamic_template_data } = await req.json();
console.log("Received payload:", { to, subject, text, html, template_id, dynamic_template_data });
if (!to) {
return new Response(JSON.stringify({
success: false,
error: "'to' email is required"
}), { status: 400, headers: corsHeaders() });
}
const payload: Record<string, any> = {
personalizations: [
{
to: [{ email: to }],
...(template_id && dynamic_template_data
? { dynamic_template_data }
: subject ? { subject } : {})
}
],
from: {
email: FROM_EMAIL,
name: "Project Name"
}
};
if (template_id) {
payload.template_id = template_id;
} else {
// Fallback to manual content if no template used
if (!subject || (!text && !html)) {
return new Response(JSON.stringify({
success: false,
error: "Missing 'subject' and 'text' or 'html' for non-template email"
}), { status: 400, headers: corsHeaders() });
}
payload.content = [
{
type: "text/plain",
value: text || "Email content"
},
...(html ? [{
type: "text/html",
value: html
}] : [])
];
}
const sendgridRes = await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: {
Authorization: `Bearer ${SENDGRID_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (!sendgridRes.ok) {
const errorBody = await sendgridRes.text();
console.error("SendGrid error:", errorBody);
return new Response(JSON.stringify({
success: false,
error: errorBody
}), {
status: sendgridRes.status,
headers: corsHeaders()
});
}
return new Response(JSON.stringify({
success: true,
message: "Email sent"
}), {
status: 200,
headers: corsHeaders()
});
} catch (err) {
console.error("Unexpected error:", err);
return new Response(JSON.stringify({
success: false,
error: err.message
}), {
status: 500,
headers: corsHeaders()
});
}
});
and then then you call this from WeWeb, you’re using doing a call with the following - this will call whatever dynamic template you want to use and parse data for that template.
curl -L -X POST 'https://sdfsdfsd.supabase.co/functions/v1/send-email' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhY32rdfsdfsdf' \
-H 'Content-Type: application/json' \
--data '{
"to": "user@example.com",
"template_id": "d-1234567890abcdef1234567890abcdef",
"dynamic_template_data": {
"first_name": "John",
"data1": "XR Software Engineer",
"cta_url": "https://domain.com"
}
}'