Every account now comes with a free mailbox — warmed automatically, no domain to buy. See how it works
Supabase does not send app email for you beyond its own auth messages, so transactional mail from a Supabase project goes through an Edge Function. The function below takes a recipient, subject and body, adds an idempotency key, and sends it with one request to Warmerly's REST API from the Deno runtime.
Four steps, the same in every framework. Warmerly is REST only today: no SMTP relay, webhooks, templates or attachments yet.
In Warmerly open Transactional email, add the domain you send from (for example mail.example.com), publish the DNS records it shows at your DNS provider, then verify. Use a domain or subdomain you do not use for cold outreach.
Under Transactional email, API keys, create a key with the Sending permission, optionally limited to that domain. It starts with wm_tx_ and is shown once, so copy it straight into your secrets store.
Set it as a function secret with the Supabase CLI: supabase secrets set WARMERLY_TX_KEY=wm_tx_xxxx. Then deploy with supabase functions deploy send-email.
A successful request answers 202 with the email's id and a status of sent (or queued, which Warmerly retries for up to 24 hours). The free plan is 5,000 emails a month with no card.
Copy these into your project. The request is the same one the API reference documents; nothing is hidden behind a package.
// supabase/functions/send-email/index.ts
const ENDPOINT = "https://app.warmerly.com/api/v1/transactional/emails";
type Payload = { to: string; subject: string; html?: string; text?: string; requestId: string };
Deno.serve(async (req: Request) => {
if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });
const { to, subject, html, text, requestId } = (await req.json()) as Payload;
const res = await fetch(ENDPOINT, {
method: "POST",
headers: {
Authorization: `Bearer ${Deno.env.get("WARMERLY_TX_KEY")}`,
"Content-Type": "application/json",
// Reuse the same requestId when you retry the same message and it is sent once.
"Idempotency-Key": requestId,
},
body: JSON.stringify({ from: "Acme <noreply@mail.example.com>", to, subject, html, text }),
});
const body = await res.json().catch(() => ({}));
return new Response(JSON.stringify(body), {
status: res.status === 202 ? 200 : 502,
headers: { "Content-Type": "application/json" },
});
});
// From your app (supabase-js), or call it from a Database Webhook on insert.
const { data, error } = await supabase.functions.invoke("send-email", {
body: {
to: "jane@example.org",
subject: "Welcome",
text: "Thanks for signing up.",
requestId: `welcome-${user.id}`,
},
});
Every refusal has a code in the response body, at error.code. These are the ones to handle, and whether to retry.
| Status | Code | Meaning | Retry? |
|---|---|---|---|
| 400 | bad_request | Invalid body, or the From address is not on a verified sending domain. | No: fix the request |
| 403 | forbidden | The key is limited to a different sending domain. | No |
| 403 | sending_paused | Sending is paused for the workspace (bounce or complaint rate, or under review). | No: contact support |
| 422 | content_rejected | The message was refused by abuse screening. | No, not unchanged |
| 429 | rate_limited | The hourly or daily sending limit was reached. | Later |
| 429 | quota_exceeded | The plan's monthly allowance, plus the free buffer, is used up. | No: upgrade or wait |
| 502 | send_failed | It could not be sent or queued. details.interrupted says whether it may have been delivered. | Same key if interrupted is false |
| 503 | service_unavailable | Transactional sending is not enabled for the workspace yet. | No |
Full details, including the sending limits, are in the transactional email API reference.
Make a fetch request to your email provider's API from the function. With Warmerly that is a POST to /api/v1/transactional/emails with a bearer key, as in the function above.
Not today: Supabase Auth sends over SMTP and Warmerly's transactional API is REST only.
No. Warmerly's transactional API is one HTTP request, so there is nothing to install. The code on this page is the whole integration.
Not yet. The send request takes from, to, cc, bcc, reply_to, subject, html or text, headers and tags. Attachments, SMTP relay, delivery webhooks and templates are not available today.
If nothing was handed over, the email is queued rather than dropped. The request still answers 202 with a status of queued, and Warmerly retries it after 1, 5, 15 and 60 minutes, then hourly, for up to 24 hours.
Create a free account, verify a sending domain and send a test. 5,000 emails a month free, no card.