Next.js integration: send transactional email
Updated
There is no Inboxili Next.js-specific package. You can use the official Node SDK `@inboxili/node` (see its page) or the plain HTTP code below.
In Next.js the rule is simple: the Inboxili API key lives in server-only code. Browsers call your route handler or server action, and that code calls Inboxili.
Environment
.env.local (never prefix with NEXT_PUBLIC_):
INBOXILI_API_KEY=ik_live_...
A server-only helper
lib/inboxili.ts:
import "server-only";
export class InboxiliError extends Error {
constructor(public status: number, public code: string, message: string) {
super(message);
}
}
type Message = {
to: string;
from_email: string;
from_name?: string;
subject?: string;
html_body?: string;
text_body?: string;
template_id?: string;
template_data?: Record<string, string | number>;
};
export async function sendEmail(message: Message): Promise<{ status: string; message_id: string | null }> {
const res = await fetch("https://api.inboxili.com/api/v1/transactional/send", {
method: "POST",
cache: "no-store",
signal: AbortSignal.timeout(10_000),
headers: {
Authorization: `Bearer ${process.env.INBOXILI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(message),
});
const body = await res.json().catch(() => ({}));
if (!res.ok) throw new InboxiliError(res.status, body.error?.code ?? "http_error", body.error?.message ?? res.statusText);
return body;
}
The server-only import makes the build fail if a client component ever imports this file. Install it with npm i server-only.
A route handler
app/api/contact/route.ts:
import { NextResponse } from "next/server";
import { InboxiliError, sendEmail } from "@/lib/inboxili";
const esc = (s: string) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
export async function POST(req: Request) {
const { name, email, message } = await req.json();
if (typeof email !== "string" || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email) || typeof message !== "string" || message.length > 5000) {
return NextResponse.json({ error: "Invalid input" }, { status: 400 });
}
try {
await sendEmail({
to: "support@yourdomain.com",
from_email: "website@yourdomain.com",
from_name: "Website",
subject: "New contact message",
html_body: "<p><strong>{{name}}</strong> ({{email}}) wrote:</p><p>{{message}}</p>",
template_data: { name: esc(String(name ?? "")), email: esc(email), message: esc(message) },
});
return NextResponse.json({ ok: true });
} catch (err) {
if (err instanceof InboxiliError && err.status === 429) {
return NextResponse.json({ error: "Busy, try again shortly" }, { status: 503 });
}
console.error("inboxili send failed", err);
return NextResponse.json({ error: "Could not send" }, { status: 502 });
}
}
Everything that comes from the visitor is escaped before it goes into the HTML. This also stops a visitor from using your form to send arbitrary email: the recipient and sender are fixed in code.
Rate-limit public forms
A public contact form that triggers an API call is an abuse target, and each key allows 120 requests per minute. Add per-IP limiting (for example Upstash Ratelimit or middleware) in front of any route that sends mail.
Server action variant
"use server";
import { sendEmail } from "@/lib/inboxili";
export async function requestLoginCode(email: string) {
const code = String(crypto.getRandomValues(new Uint32Array(1))[0] % 1_000_000).padStart(6, "0");
// store a hash of `code` with an expiry, then:
await sendEmail({
to: email,
from_email: "security@yourdomain.com",
subject: "Your code",
html_body: "<p>Your code is <strong>{{code}}</strong></p>",
text_body: `Your code is ${code}`,
template_data: { code },
});
}
See the OTP workflow for storing and checking the code.
Testing
Mock the helper in your unit tests with your test runner's module mocking, and keep a single manual end-to-end check against a real verified domain in staging.
Production notes
- Add
INBOXILI_API_KEYas a secret in your hosting provider, not in the repo. - Route handlers on serverless platforms have execution limits. Keep the call to one request and return quickly.
- A timeout does not tell you whether the email went out. The API has no idempotency key.
Frequently asked questions
- Can I call the API from a client component?
- No. The API key would be exposed to every visitor. Call it from a route handler, server action or server component only.
Build with Inboxili
Create a workspace, verify a domain, and make your first API call.