Skip to content

Documentation

Integration guides.

Runnable examples for the frameworks and services people use most.

Integration guides

Each example below was run against the API before it was published, with the mailtruss package. Replace yourdomain.com with a domain you have verified. To try any of them before your DNS is ready, send to delivered@mailtruss.test (see test mode above).

Next.js

A route handler in the App Router. It escapes user input, reports Mailtruss errors instead of throwing, and uses an idempotency key so a retried request never sends twice.

app/api/welcome/route.ts
import { MailClient, MailError } from "mailtruss";

const mail = new MailClient({ apiKey: process.env.MAILTRUSS_API_KEY! });
const escapeHtml = (s: string) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);

export async function POST(req: Request) {
  const { email, name } = await req.json();
  try {
    const { id } = await mail.emails.send(
      {
        from: "Acme <hello@yourdomain.com>",
        to: email,
        subject: "Welcome to Acme",
        html: `<p>Hi ${escapeHtml(name)}, thanks for signing up.</p>`,
      },
      { idempotencyKey: `welcome:${email}` }, // a retry of this request sends nothing twice
    );
    return Response.json({ id });
  } catch (e) {
    if (e instanceof MailError) {
      const status = e.statusCode === 429 ? 503 : 502;
      return Response.json({ error: e.code, message: e.message }, { status });
    }
    throw e;
  }
}

React Email

Render a React Email component to HTML and plain text with @react-email/render, then send both parts. Install @react-email/components, @react-email/render and react.

welcome-email.tsx
import { Body, Button, Container, Heading, Html, Text } from "@react-email/components";
import { render } from "@react-email/render";
import { MailClient } from "mailtruss";

function Welcome({ name }: { name: string }) {
  return (
    <Html>
      <Body style={{ fontFamily: "sans-serif" }}>
        <Container>
          <Heading>Welcome, {name}</Heading>
          <Text>Your account is ready.</Text>
          <Button
            href="https://yourdomain.com/start"
            style={{ background: "#533afd", color: "#fff", padding: "10px 16px" }}
          >
            Get started
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

const mail = new MailClient({ apiKey: process.env.MAILTRUSS_API_KEY! });
const element = <Welcome name="Ada" />;

export const sent = await mail.emails.send({
  from: "Acme <hello@yourdomain.com>",
  to: "delivered@mailtruss.test",
  subject: "Welcome to Acme",
  html: await render(element),
  text: await render(element, { plainText: true }),
});

Node.js and Express: receiving webhooks

Read the raw request body, as express.json() would re-serialise it and break the signature. verifyWebhook checks the signature and the timestamp and returns the parsed event. Set MAILTRUSS_WEBHOOK_SECRET to the endpoint’s signing secret.

server.js
import express from "express";
import { verifyWebhook } from "mailtruss";

const app = express();

// Verify the signature over the RAW body: express.json() would re-serialise it
// and break the signature.
app.post("/webhooks/mailtruss", express.raw({ type: "application/json" }), async (req, res) => {
  try {
    const secret = process.env.MAILTRUSS_WEBHOOK_SECRET;
    const event = await verifyWebhook(req.body.toString("utf8"), req.headers, secret);
    if (event.type === "email.bounced") console.log("bounced:", event.data.email_id);
    res.sendStatus(200); // any 2xx within 10 seconds counts as delivered
  } catch {
    res.sendStatus(400);
  }
});

export default app;

Supabase Auth emails

Supabase can hand every auth email (sign-up confirmation, password reset, magic link) to your own endpoint through the Send Email hook. This Edge Function follows Supabase’s documented payload and signing: it verifies the hook, builds the confirmation link the way their docs show, and sends it. Click tracking is switched off, so auth links are never rewritten. Create the hook secret in the Supabase dashboard (Authentication, Hooks), and set MAILTRUSS_API_KEY, SEND_EMAIL_HOOK_SECRET and SUPABASE_PROJECT_REF as function secrets. The code is tested with the documented payload; deploy it to a test project and confirm one sign-up before relying on it.

supabase/functions/send-email/index.ts
// Supabase Edge Function for Auth > Hooks > Send Email.
// In Deno, import from "npm:mailtruss" and "npm:standardwebhooks".
import { Webhook } from "standardwebhooks";
import { MailClient } from "mailtruss";

const mail = new MailClient({ apiKey: process.env.MAILTRUSS_API_KEY! });
// Supabase shows the secret as "v1,whsec_<base64>"; the library wants only the base64 part.
const hook = new Webhook(process.env.SEND_EMAIL_HOOK_SECRET!.replace("v1,whsec_", ""));

const SUBJECTS: Record<string, string> = {
  signup: "Confirm your email",
  recovery: "Reset your password",
  magiclink: "Your sign-in link",
  email_change: "Confirm your new email",
  invite: "You have been invited",
};

type HookPayload = {
  user: { email: string };
  email_data: { token_hash: string; email_action_type: string; redirect_to: string };
};

export async function handler(req: Request): Promise<Response> {
  if (req.method !== "POST") return new Response("not allowed", { status: 405 });
  let payload: HookPayload;
  try {
    payload = hook.verify(await req.text(), Object.fromEntries(req.headers)) as HookPayload;
  } catch {
    return new Response("invalid signature", { status: 401 });
  }
  const { user, email_data } = payload;
  const verify = new URLSearchParams({
    token: email_data.token_hash,
    type: email_data.email_action_type,
    redirect_to: email_data.redirect_to,
  });
  const link = `https://${process.env.SUPABASE_PROJECT_REF}.supabase.co/auth/v1/verify?${verify}`;
  try {
    await mail.emails.send(
      {
        from: "Acme <auth@yourdomain.com>",
        to: user.email,
        subject: SUBJECTS[email_data.email_action_type] ?? "Action required",
        html: `<p><a href="${link}">Continue</a></p>`,
        text: `Continue: ${link}`,
        tracking: false, // never rewrite auth links through a click tracker
      },
      { idempotencyKey: `auth:${email_data.token_hash}` },
    );
  } catch (e) {
    const body = JSON.stringify({ error: { http_code: 500, message: String(e) } });
    return new Response(body, { status: 500, headers: { "content-type": "application/json" } });
  }
  return new Response("{}", { status: 200, headers: { "content-type": "application/json" } });
}