Every account now comes with a free mailbox — warmed automatically, no domain to buy. See how it works
In the App Router the right place to send email is a route handler (or a server action): it runs on the server, so the API key never reaches the browser. This route sends a password-reset email through Warmerly's REST API and returns a small JSON answer your client code can use.
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.
Add it to .env.local for development and to your host's environment settings for production. Do not prefix it with NEXT_PUBLIC_, or Next.js will inline it into the browser bundle.
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.
// app/api/send-reset/route.ts
import { NextResponse } from "next/server";
const ENDPOINT = "https://app.warmerly.com/api/v1/transactional/emails";
type ResetRequest = { email: string; resetUrl: string; requestId: string };
export async function POST(request: Request) {
const { email, resetUrl, requestId } = (await request.json()) as ResetRequest;
const res = await fetch(ENDPOINT, {
method: "POST",
headers: {
// Server only. Never prefix this with NEXT_PUBLIC_ and never call this from a client component.
Authorization: `Bearer ${process.env.WARMERLY_TX_KEY}`,
"Content-Type": "application/json",
// One key per reset request, so a retry of the same request cannot send a second email.
"Idempotency-Key": `reset-${requestId}`,
},
body: JSON.stringify({
from: "Acme <noreply@mail.example.com>",
to: email,
subject: "Reset your password",
html: `<p>Click <a href="${resetUrl}">here</a> to reset your password.</p>`,
text: `Reset your password: ${resetUrl}`,
}),
});
if (res.status !== 202) {
const body = (await res.json().catch(() => ({}))) as { error?: { code?: string } };
console.error("Warmerly send failed", res.status, body.error?.code);
return NextResponse.json({ error: body.error?.code ?? "send_failed" }, { status: 502 });
}
const sent = (await res.json()) as { id: string; status: "sent" | "queued"; duplicate: boolean };
return NextResponse.json({ ok: true, id: sent.id, status: sent.status });
}
// In a client component: call YOUR route, never Warmerly directly.
await fetch("/api/send-reset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, resetUrl, requestId: crypto.randomUUID() }),
});
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.
No. A client component runs in the browser, so the API key would be public. Call your own route handler or server action instead, and keep the key in a server-only environment variable.
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.