Integrations
Supabase Auth emails
Send Supabase sign-up, magic link, password reset, and email change messages through SendHQ with Supabase's Send Email hook and an Edge Function.
How it works
Supabase Auth can hand every authentication email to an HTTPS endpoint through its Send Email hook instead of sending it itself. A small Supabase Edge Function receives the hook, verifies its signature, and sends the message through the SendHQ API from your verified domain. Confirmation links, magic links, and one-time codes stay generated by Supabase; SendHQ only delivers them.
1. Store the secrets
Generate the hook secret in the Supabase dashboard under Authentication → Hooks, then set both secrets on the project.
SENDHQ_API_KEY="re_your_key_here"
SEND_EMAIL_HOOK_SECRET="v1,whsec_<base64_secret>"supabase secrets set --env-file .env
supabase functions new send-email2. Write the Edge Function
The function verifies the Standard Webhooks signature, builds the verification link from the token hash, and calls SendHQ. Send each email with an idempotency key derived from the token hash so a retried hook never sends twice.
import { Webhook } from "https://esm.sh/standardwebhooks@1.0.0";
const hookSecret = Deno.env.get("SEND_EMAIL_HOOK_SECRET")!.replace("v1,whsec_", "");
const apiKey = Deno.env.get("SENDHQ_API_KEY")!;
const projectUrl = Deno.env.get("SUPABASE_URL")!;
const subjects: Record<string, string> = {
signup: "Confirm your email",
magiclink: "Your sign-in link",
recovery: "Reset your password",
invite: "You have been invited",
email_change: "Confirm your new email address",
};
Deno.serve(async (req) => {
const payload = await req.text();
let event;
try {
event = new Webhook(hookSecret).verify(payload, Object.fromEntries(req.headers));
} catch {
return Response.json({ error: { http_code: 401, message: "Invalid signature" } }, { status: 401 });
}
const { user, email_data } = event as any;
const link = `${projectUrl}/auth/v1/verify?` + new URLSearchParams({
token: email_data.token_hash,
type: email_data.email_action_type,
redirect_to: email_data.redirect_to,
});
const res = await fetch("https://sendhq.cc/api/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": `supabase-${email_data.token_hash}`,
},
body: JSON.stringify({
from: "Acme <auth@example.com>",
to: [user.email],
subject: subjects[email_data.email_action_type] ?? "Your account",
html: `<p><a href="${link}">Continue</a></p><p>Or enter this code: ${email_data.token}</p>`,
text: `Continue: ${link}\nOr enter this code: ${email_data.token}`,
}),
});
if (!res.ok) {
return Response.json({ error: { http_code: res.status, message: await res.text() } }, { status: 500 });
}
return Response.json({});
});3. Deploy and enable the hook
supabase functions deploy send-email --no-verify-jwtIn Authentication → Hooks, add a Send Email hook of type HTTPS pointing at the function URL, paste the same secret, and enable it. Supabase now calls the function instead of sending authentication email itself.
Email change and other action types
email_action_type tells you which message to send: signup, invite, magiclink, recovery, email_change, reauthentication, and notification types such as password_changed_notification. With Secure Email Change on, an email_change event can carry two codes: send token with token_hash_new to the current address and token_new with token_hash to the new one.
Check delivery
The SendHQ response contains the email ID. Look it up with GET /api/v1/emails/:id/events, the dashboard, or sendhq email events em_123 to see delivery, bounce, and complaint events. Addresses that hard-bounce are suppressed automatically, which protects your domain's reputation when users mistype their email at sign-up.